From 3386dbe4538c3915f47883625013876d22f2a35c Mon Sep 17 00:00:00 2001 From: builder Date: Fri, 10 Jul 2026 15:05:55 +0000 Subject: [PATCH] Samples: Automatic updates to public repository Remember to do the following: 1. Ensure that modified/deleted/new files are correct 2. Make this commit message relevant for the changes 3. Force push 4. Delete branch after PR is merged If this commit is an update from one SDK version to another, make sure to create a release tag for previous version. --- .github/workflows/main.yml | 4 +- README.md | 37 +- continuous-integration/lint.sh | 2 +- continuous-integration/requirements.txt | 2 +- continuous-integration/setup.sh | 7 +- modules/zividsamples/camera_verification.py | 8 +- modules/zividsamples/display.py | 1 - .../calibration/calibration_buttons_widget.py | 12 +- .../calibration/hand_eye_calibration_gui.py | 61 +- .../calibration/hand_eye_settings_tester.py | 42 +- .../calibration/pose_pair_selection_widget.py | 73 +- .../gui/calibration/set_fixed_objects.py | 20 +- ...nfield_correction_data_selection_widget.py | 81 ++- .../gui/preparation/infield_correction_gui.py | 40 +- .../infield_correction_result_widget.py | 10 +- .../gui/preparation/warmup_gui.py | 76 +- modules/zividsamples/gui/qt_application.py | 14 +- .../zividsamples/gui/robot/robot_control.py | 4 +- .../gui/robot/robot_control_robodk.py | 20 +- .../robot/robot_control_ur_rtde_read_only.py | 6 +- .../gui/robot/robot_control_widget.py | 50 +- .../capture_at_pose_selection_widget.py | 44 +- .../verification/hand_eye_verification_gui.py | 50 +- .../gui/verification/stitch_gui.py | 40 +- .../gui/verification/touch_gui.py | 36 +- .../verification_buttons_widget.py | 10 +- .../gui/widgets/aspect_ratio_label.py | 30 +- .../gui/widgets/camera_buttons_widget.py | 18 +- .../zividsamples/gui/widgets/cv2_handler.py | 22 +- .../gui/widgets/detection_visualization.py | 15 +- modules/zividsamples/gui/widgets/fov.py | 20 +- .../zividsamples/gui/widgets/image_viewer.py | 18 +- .../gui/widgets/live_2d_widget.py | 16 +- .../gui/widgets/pointcloud_visualizer.py | 8 +- .../zividsamples/gui/widgets/pose_widget.py | 84 +-- .../gui/widgets/show_yaml_dialog.py | 2 +- .../gui/widgets/tab_content_widget.py | 42 +- .../gui/widgets/tab_with_robot_support.py | 18 +- .../gui/widgets/tutorial_widget.py | 18 +- .../gui/wizard/camera_selection.py | 32 +- .../zividsamples/gui/wizard/data_directory.py | 72 +- .../gui/wizard/hand_eye_configuration.py | 16 +- .../gui/wizard/marker_configuration.py | 32 +- .../gui/wizard/robot_configuration.py | 6 +- .../wizard/rotation_format_configuration.py | 26 +- .../gui/wizard/settings_selector.py | 22 +- .../gui/wizard/touch_configuration.py | 11 +- .../save_load_transformation_matrix.py | 3 - modules/zividsamples/settings_utils.py | 2 + requirements.txt | 2 +- .../advanced/barcode/barcode_detector.py | 42 +- ...ure_and_process_image_with_cupy_on_cuda.py | 116 +++ ..._render_point_cloud_with_opengl_on_cuda.py | 688 ++++++++++++++++++ ..._and_segment_image_with_pytorch_on_cuda.py | 301 ++++++++ .../applications/advanced/mask_point_cloud.py | 63 ++ .../advanced/roi/roi_box_via_aruco_marker.py | 32 +- .../advanced/roi/roi_box_via_checkerboard.py | 33 +- .../stitch_using_robot_mounted_camera.py | 44 +- .../capture_from_file_camera_vis_3d.py | 42 +- .../camera/basic/capture_from_file_camera.py | 19 +- .../basic/capture_with_settings_from_yml.py | 17 + source/camera/basic/connect.py | 58 ++ ...e_file_camera_from_zdf_with_diagnostics.py | 74 ++ source/camera/info_util_other/camera_info.py | 13 +- .../capture_with_diagnostics.py | 19 +- source/camera/info_util_other/check_health.py | 57 ++ ...matic_network_configuration_for_cameras.py | 66 +- 67 files changed, 2304 insertions(+), 665 deletions(-) create mode 100644 source/applications/advanced/cuda/capture_and_process_image_with_cupy_on_cuda.py create mode 100644 source/applications/advanced/cuda/capture_and_render_point_cloud_with_opengl_on_cuda.py create mode 100644 source/applications/advanced/cuda/capture_and_segment_image_with_pytorch_on_cuda.py create mode 100644 source/applications/advanced/mask_point_cloud.py create mode 100644 source/camera/basic/connect.py create mode 100644 source/camera/basic/create_file_camera_from_zdf_with_diagnostics.py create mode 100644 source/camera/info_util_other/check_health.py diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 44bc2a03..881758b2 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - ubuntu-version: ["20.04", "22.04"] + ubuntu-version: ["latest"] steps: - name: Check out code uses: actions/checkout@v2 @@ -29,7 +29,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - ubuntu-version: ["20.04"] + ubuntu-version: ["latest"] steps: - name: Check out code uses: actions/checkout@v2 diff --git a/README.md b/README.md index 6f492c00..260f3891 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Python samples -This repository contains python code samples for Zivid SDK v2.17.2. For +This repository contains python code samples for Zivid SDK v2.18.0. For tested compatibility with earlier SDK versions, please check out [accompanying releases]. @@ -27,12 +27,13 @@ tested compatibility with earlier SDK versions, please check out - **Capture** - [Quick Capture Tutorial] - [Capture Tutorial] + - [GPU Access Tutorial] - [Point Cloud Capture Process] - [2D Image Capture Process] - [2D + 3D Capture Strategy] - [File Camera] - [Projector] - - **Maintenance** + - **Maintenance and Prevention** - [Infield Correction] - [Warm-up] - [Firmware Update] @@ -71,6 +72,10 @@ from the camera can be used. - [capture_with_settings_from_yml] - Capture images and point clouds, with and without color, from the Zivid camera with settings from YML file. + - [connect] - Connect to a Zivid camera using the different + available methods. + - [create_file_camera_from_zdf_with_diagnostics] - Capture a frame + with diagnostics enabled and create a file camera from it. - **advanced** - [capture_2d_and_3d] - Capture 2D and 3D with the Zivid camera. - [capture_and_print_normals] - Capture Zivid point clouds, compute @@ -82,9 +87,11 @@ from the camera can be used. and Zivid SDK, then list cameras and print camera info and state for each connected camera. - [camera_user_data] - Store user data on the Zivid camera. - - [capture_with_diagnostics] - Capture point clouds, with color, - from the Zivid camera, with default settings and diagnostics - enabled. + - [capture_with_diagnostics] - Capture a 2D+3D frame and a 2D frame + from the Zivid camera with diagnostics enabled. + - [check_health] - Poll the camera health check from a separate + thread while capturing in the main thread, printing the statuses + and values every second. - [context_manager_with_zivid] - Sample showing how to use a context manager with Zivid Application and safely return processed data. - [firmware_updater] - Update firmware on the Zivid camera. @@ -139,12 +146,24 @@ from the camera can be used. convert it to OpenCV format, then extract and visualize depth map. - [downsample][1] - Downsample point cloud from a ZDF file. - [gamma_correction] - Capture 2D image with gamma correction. + - [mask_point_cloud] - Read point cloud data from a ZDF file, apply + a binary mask, and visualize it. - **visualization** - [capture_vis_3d_in_loop] - Capture point clouds, with color, from the Zivid camera, and visualize them in a loop. - [capture_vis_3d_in_loop_with_keypress_exit] - Capture point clouds, with color, from the Zivid camera, and visualize them in a loop. Press 'q' to exit. + - **cuda** + - [capture_and_process_image_with_cupy_on_cuda] - Demonstrate GPU + interop with CuPy: wrap a Zivid GPU image buffer as a CuPy array + without copying it through CPU memory. + - [capture_and_render_point_cloud_with_opengl_on_cuda] - + Demonstrate GPU point cloud rendering with CUDA-OpenGL interop: + capture a Zivid point cloud, copy it device-to-device + - [capture_and_segment_image_with_pytorch_on_cuda] - Demonstrate + zero-copy GPU interop between Zivid and PyTorch/CuPy by feeding + a Zivid 2D image into a third-party - **transform** - [get_checkerboard_pose_from_zdf] - Read point cloud data of a Zivid calibration board from a ZDF file, estimate the @@ -312,6 +331,7 @@ Zivid Samples are distributed under the [BSD license]. [image]: https://www.zivid.com/hubfs/softwarefiles/images/zivid-generic-github-header.png [Quick Capture Tutorial]: https://support.zivid.com/en/latest/camera/getting-started/quick-capture-tutorial.html [Capture Tutorial]: https://support.zivid.com/en/latest/camera/academy/camera/capture-tutorial.html + [GPU Access Tutorial]: https://support.zivid.com/en/latest/camera/academy/camera/gpu-access-tutorial.html [Point Cloud Capture Process]: https://support.zivid.com/en/latest/camera/academy/camera/point-cloud-capture-process.html [2D Image Capture Process]: https://support.zivid.com/en/latest/camera/academy/camera/2d-image-capture-process.html [2D + 3D Capture Strategy]: https://support.zivid.com/en/latest/camera/academy/camera/2d3d-capture-strategy.html @@ -334,12 +354,15 @@ Zivid Samples are distributed under the [BSD license]. [capture_from_file_camera]: https://github.com/zivid/zivid-python-samples/tree/master/source/camera/basic/capture_from_file_camera.py [capture_hdr_complete_settings]: https://github.com/zivid/zivid-python-samples/tree/master/source/camera/basic/capture_hdr_complete_settings.py [capture_with_settings_from_yml]: https://github.com/zivid/zivid-python-samples/tree/master/source/camera/basic/capture_with_settings_from_yml.py + [connect]: https://github.com/zivid/zivid-python-samples/tree/master/source/camera/basic/connect.py + [create_file_camera_from_zdf_with_diagnostics]: https://github.com/zivid/zivid-python-samples/tree/master/source/camera/basic/create_file_camera_from_zdf_with_diagnostics.py [capture_2d_and_3d]: https://github.com/zivid/zivid-python-samples/tree/master/source/camera/advanced/capture_2d_and_3d.py [capture_and_print_normals]: https://github.com/zivid/zivid-python-samples/tree/master/source/camera/advanced/capture_and_print_normals.py [adapt_settings_for_flickering_ambient_light]: https://github.com/zivid/zivid-python-samples/tree/master/source/camera/info_util_other/adapt_settings_for_flickering_ambient_light.py [camera_info]: https://github.com/zivid/zivid-python-samples/tree/master/source/camera/info_util_other/camera_info.py [camera_user_data]: https://github.com/zivid/zivid-python-samples/tree/master/source/camera/info_util_other/camera_user_data.py [capture_with_diagnostics]: https://github.com/zivid/zivid-python-samples/tree/master/source/camera/info_util_other/capture_with_diagnostics.py + [check_health]: https://github.com/zivid/zivid-python-samples/tree/master/source/camera/info_util_other/check_health.py [context_manager_with_zivid]: https://github.com/zivid/zivid-python-samples/tree/master/source/camera/info_util_other/context_manager_with_zivid.py [firmware_updater]: https://github.com/zivid/zivid-python-samples/tree/master/source/camera/info_util_other/firmware_updater.py [frame_info]: https://github.com/zivid/zivid-python-samples/tree/master/source/camera/info_util_other/frame_info.py @@ -365,8 +388,12 @@ Zivid Samples are distributed under the [BSD license]. [create_depth_map]: https://github.com/zivid/zivid-python-samples/tree/master/source/applications/advanced/create_depth_map.py [1]: https://github.com/zivid/zivid-python-samples/tree/master/source/applications/advanced/downsample.py [gamma_correction]: https://github.com/zivid/zivid-python-samples/tree/master/source/applications/advanced/gamma_correction.py + [mask_point_cloud]: https://github.com/zivid/zivid-python-samples/tree/master/source/applications/advanced/mask_point_cloud.py [capture_vis_3d_in_loop]: https://github.com/zivid/zivid-python-samples/tree/master/source/applications/advanced/visualization/capture_vis_3d_in_loop.py [capture_vis_3d_in_loop_with_keypress_exit]: https://github.com/zivid/zivid-python-samples/tree/master/source/applications/advanced/visualization/capture_vis_3d_in_loop_with_keypress_exit.py + [capture_and_process_image_with_cupy_on_cuda]: https://github.com/zivid/zivid-python-samples/tree/master/source/applications/advanced/cuda/capture_and_process_image_with_cupy_on_cuda.py + [capture_and_render_point_cloud_with_opengl_on_cuda]: https://github.com/zivid/zivid-python-samples/tree/master/source/applications/advanced/cuda/capture_and_render_point_cloud_with_opengl_on_cuda.py + [capture_and_segment_image_with_pytorch_on_cuda]: https://github.com/zivid/zivid-python-samples/tree/master/source/applications/advanced/cuda/capture_and_segment_image_with_pytorch_on_cuda.py [get_checkerboard_pose_from_zdf]: https://github.com/zivid/zivid-python-samples/tree/master/source/applications/advanced/transform/get_checkerboard_pose_from_zdf.py [transform_point_cloud_from_millimeters_to_meters]: https://github.com/zivid/zivid-python-samples/tree/master/source/applications/advanced/transform/transform_point_cloud_from_millimeters_to_meters.py [transform_point_cloud_via_aruco_marker]: https://github.com/zivid/zivid-python-samples/tree/master/source/applications/advanced/transform/transform_point_cloud_via_aruco_marker.py diff --git a/continuous-integration/lint.sh b/continuous-integration/lint.sh index 787820ba..111f5caa 100755 --- a/continuous-integration/lint.sh +++ b/continuous-integration/lint.sh @@ -4,7 +4,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ROOT_DIR=$(realpath "$SCRIPT_DIR/..") SOURCE_DIR=$(realpath "$ROOT_DIR/source") -python3 -m pip install --requirement "$SCRIPT_DIR/requirements.txt" || exit +python3 -m pip install --break-system-packages --requirement "$SCRIPT_DIR/requirements.txt" || exit pythonFiles=$(find "$SOURCE_DIR" -name '*.py' -not -path "*/ur_hand_eye_calibration/3rdParty*") diff --git a/continuous-integration/requirements.txt b/continuous-integration/requirements.txt index 82d2a4a6..7ab5127e 100644 --- a/continuous-integration/requirements.txt +++ b/continuous-integration/requirements.txt @@ -1,5 +1,5 @@ black==24.1.0 flake8==7.0.0 -flake8-annotations==3.0.1 +flake8-annotations==3.2.0 pylint==3.0.3 darglint==1.8.1 \ No newline at end of file diff --git a/continuous-integration/setup.sh b/continuous-integration/setup.sh index f4fc8d13..1f8cc321 100755 --- a/continuous-integration/setup.sh +++ b/continuous-integration/setup.sh @@ -29,12 +29,11 @@ function install_www_deb { rm -r $TMP_DIR || exit } -install_www_deb "https://downloads.zivid.com/sdk/releases/2.17.2+440b2367-1/u${VERSION_ID:0:2}/zivid_2.17.2+440b2367-1_amd64.deb" || exit -install_www_deb "https://downloads.zivid.com/sdk/releases/2.17.2+440b2367-1/u${VERSION_ID:0:2}/zivid-genicam_2.17.2+440b2367-1_amd64.deb" || exit +install_www_deb "https://downloads.zivid.com/sdk/releases/2.18.0+1b44dbef-1/u${VERSION_ID:0:2}/zivid-opencl_2.18.0+1b44dbef-1_amd64.deb" || exit +install_www_deb "https://downloads.zivid.com/sdk/releases/2.18.0+1b44dbef-1/u${VERSION_ID:0:2}/zivid-genicam_2.18.0+1b44dbef-1_amd64.deb" || exit -python3 -m pip install --upgrade pip || exit pushd "$ROOT_DIR" || exit -python3 -m pip install --requirement "./requirements.txt" || exit +python3 -m pip install --break-system-packages --requirement "./requirements.txt" || exit popd || exit echo Success! ["$(basename $0)"] diff --git a/modules/zividsamples/camera_verification.py b/modules/zividsamples/camera_verification.py index 399a3ca0..83782625 100644 --- a/modules/zividsamples/camera_verification.py +++ b/modules/zividsamples/camera_verification.py @@ -1,7 +1,7 @@ import random from dataclasses import dataclass from datetime import datetime, timedelta -from typing import Optional +from typing import Optional, Tuple import zivid @@ -33,13 +33,13 @@ class VerificationAndState: time: datetime info: zivid.CameraInfo - def _position(self): + def _position(self) -> Tuple[Optional[float], Optional[float], Optional[float]]: return self.verification.position() if self.verification else (None, None, None) - def distance(self): + def distance(self) -> Optional[float]: return self._position()[2] - def local_dimension_trueness(self): + def local_dimension_trueness(self) -> Optional[float]: return self.verification.local_dimension_trueness() if self.verification else None def __str__(self) -> str: diff --git a/modules/zividsamples/display.py b/modules/zividsamples/display.py index 07856a55..fc9e5764 100644 --- a/modules/zividsamples/display.py +++ b/modules/zividsamples/display.py @@ -82,7 +82,6 @@ def display_pointcloud(data: Union[zivid.Frame, zivid.PointCloud, zivid.Unorgani Args: data: Union[zivid.Frame, zivid.PointCloud, zivid.UnorganizedPointCloud] - normals: If True, display normals as color map """ with zivid.visualization.Visualizer() as visualizer: diff --git a/modules/zividsamples/gui/calibration/calibration_buttons_widget.py b/modules/zividsamples/gui/calibration/calibration_buttons_widget.py index b01bfbdc..27978c69 100644 --- a/modules/zividsamples/gui/calibration/calibration_buttons_widget.py +++ b/modules/zividsamples/gui/calibration/calibration_buttons_widget.py @@ -1,4 +1,4 @@ -from typing import List +from typing import List, Optional from PyQt5.QtCore import pyqtSignal from PyQt5.QtWidgets import QApplication, QCheckBox, QGroupBox, QHBoxLayout, QPushButton, QWidget @@ -8,7 +8,7 @@ class HandEyeCalibrationButtonsWidget(QWidget): calibrate_button_clicked = pyqtSignal() use_fixed_objects_toggled = pyqtSignal(bool) - def __init__(self, parent=None): + def __init__(self, parent: Optional[QWidget] = None) -> None: super().__init__(parent) # Define buttons @@ -34,19 +34,19 @@ def __init__(self, parent=None): self.setLayout(buttons_layout) - def on_calibrate_button_clicked(self): + def on_calibrate_button_clicked(self) -> None: self.calibrate_button.setStyleSheet("background-color: yellow;") QApplication.processEvents() self.calibrate_button_clicked.emit() self.calibrate_button.setStyleSheet("") - def on_use_fixed_objects_toggled(self, checked: bool): + def on_use_fixed_objects_toggled(self, checked: bool) -> None: self.use_fixed_objects_toggled.emit(checked) - def disable_buttons(self): + def disable_buttons(self) -> None: self.calibrate_button.setEnabled(False) - def enable_buttons(self): + def enable_buttons(self) -> None: self.calibrate_button.setEnabled(True) def get_tab_widgets_in_order(self) -> List[QWidget]: diff --git a/modules/zividsamples/gui/calibration/hand_eye_calibration_gui.py b/modules/zividsamples/gui/calibration/hand_eye_calibration_gui.py index e6c6b972..c1d07839 100644 --- a/modules/zividsamples/gui/calibration/hand_eye_calibration_gui.py +++ b/modules/zividsamples/gui/calibration/hand_eye_calibration_gui.py @@ -66,8 +66,8 @@ def __init__( marker_configuration: MarkerConfiguration, cv2_handler: CV2Handler, initial_rotation_information: RotationInformation, - parent=None, - ): + parent: Optional[QWidget] = None, + ) -> None: super().__init__(data_directory, parent) self.description = [ @@ -96,7 +96,7 @@ def __init__( calibrated=False, ) - def create_widgets(self, initial_rotation_information: RotationInformation): + def create_widgets(self, initial_rotation_information: RotationInformation) -> None: self.robot_pose_widget = PoseWidget.Robot( eye_in_hand=self.hand_eye_configuration.eye_in_hand, display_mode=PoseWidgetDisplayMode.OnlyPose, @@ -116,7 +116,7 @@ def create_widgets(self, initial_rotation_information: RotationInformation): self.hand_eye_calibration_buttons.calibrate_button.setEnabled(False) self.hand_eye_calibration_buttons.setObjectName("HE-Calibration-hand_eye_calibration_buttons") - def setup_layout(self): + def setup_layout(self) -> None: layout = QVBoxLayout() left_panel = QVBoxLayout() right_panel = QVBoxLayout() @@ -139,7 +139,7 @@ def setup_layout(self): layout.addLayout(bottom_layout) self.setLayout(layout) - def connect_signals(self): + def connect_signals(self) -> None: self.hand_eye_calibration_buttons.calibrate_button_clicked.connect(self.on_calibrate_button_clicked) self.hand_eye_calibration_buttons.use_fixed_objects_toggled.connect(self.on_use_fixed_objects_toggled) self.confirm_robot_pose_button.clicked.connect(self.on_confirm_robot_pose_button_clicked) @@ -154,7 +154,7 @@ def _on_pose_pairs_loading_finished(self) -> None: save_to_disk = self.pose_pair_selection_widget.last_operation_was_reprocess self._calibrate(save_to_disk=save_to_disk) - def update_instructions(self, has_detection_result: bool, robot_pose_confirmed: bool, calibrated: bool): + def update_instructions(self, has_detection_result: bool, robot_pose_confirmed: bool, calibrated: bool) -> None: self.has_confirmed_robot_pose = robot_pose_confirmed self.has_detection_result = has_detection_result and self.has_confirmed_robot_pose minimum_captures_to_go = ( @@ -185,7 +185,16 @@ def _resolve_config_from_saved_session( calibration_object: CalibrationObject, marker_configuration: MarkerConfiguration, ) -> Tuple[CalibrationObject, MarkerConfiguration]: - """If saved config differs from current, ask user and return (calibration_object, marker_configuration).""" + """If saved config differs from current, ask user and return (calibration_object, marker_configuration). + + Args: + saved_config: Calibration configuration loaded from the saved session. + calibration_object: Calibration object from the current configuration. + marker_configuration: Marker configuration from the current configuration. + + Returns: + Tuple of the calibration object and marker configuration to use. + """ mismatches = [] if saved_config.calibration_object != calibration_object: mismatches.append( @@ -215,7 +224,7 @@ def _resolve_config_from_saved_session( ) return (saved_config.calibration_object, new_marker_config) - def on_pending_changes(self): + def on_pending_changes(self) -> None: self.pose_pair_selection_widget.clear() self.pose_pair_selection_widget.set_directory(self.data_directory) if not self.data_directory_has_data(): @@ -242,10 +251,10 @@ def on_pending_changes(self): def is_loading(self) -> bool: return self.pose_pair_selection_widget.is_loading() - def on_tab_visibility_changed(self, is_current: bool): + def on_tab_visibility_changed(self, is_current: bool) -> None: pass - def hand_eye_configuration_update(self, hand_eye_configuration: HandEyeConfiguration): + def hand_eye_configuration_update(self, hand_eye_configuration: HandEyeConfiguration) -> None: self.hand_eye_configuration = hand_eye_configuration self.detection_visualization_widget.on_hand_eye_configuration_updated(self.hand_eye_configuration) self.robot_pose_widget.on_eye_in_hand_toggled(self.hand_eye_configuration.eye_in_hand) @@ -255,7 +264,7 @@ def hand_eye_configuration_update(self, hand_eye_configuration: HandEyeConfigura self.fixed_objects.update_hand_eye_configuration(self.hand_eye_configuration) self._prompt_reprocess_if_needed() - def marker_configuration_update(self, marker_configuration: MarkerConfiguration): + def marker_configuration_update(self, marker_configuration: MarkerConfiguration) -> None: self.marker_configuration = marker_configuration self.fixed_objects.update_marker_configuration(self.marker_configuration) self._prompt_reprocess_if_needed() @@ -283,10 +292,10 @@ def _prompt_reprocess_if_needed(self) -> None: self.marker_configuration, ) - def rotation_format_update(self, rotation_information: RotationInformation): + def rotation_format_update(self, rotation_information: RotationInformation) -> None: self.robot_pose_widget.set_rotation_format(rotation_information) - def robot_configuration_update(self, robot_configuration: RobotConfiguration): + def robot_configuration_update(self, robot_configuration: RobotConfiguration) -> None: self.robot_configuration = robot_configuration self.confirm_robot_pose_button.setVisible(self.robot_configuration.has_no_robot()) self.update_instructions( @@ -295,12 +304,12 @@ def robot_configuration_update(self, robot_configuration: RobotConfiguration): calibrated=False, ) - def on_select_fixed_objects_action_triggered(self): + def on_select_fixed_objects_action_triggered(self) -> None: updated_fixed_objects = set_fixed_objects(self.fixed_objects, self.robot_pose_widget.rotation_information) if updated_fixed_objects is not None: self.fixed_objects = updated_fixed_objects - def toggle_advanced_view(self, checked): + def toggle_advanced_view(self, checked: bool) -> None: self.robot_pose_widget.toggle_advanced_section(checked) def on_start_auto_run(self) -> bool: @@ -317,18 +326,18 @@ def on_start_auto_run(self) -> bool: return True return False - def on_pose_pair_clicked(self, pose_pair: PosePair): + def on_pose_pair_clicked(self, pose_pair: PosePair) -> None: self.pose_pair = pose_pair self.robot_pose_widget.set_transformation_matrix(self.pose_pair.robot_pose) self.detection_visualization_widget.set_image(self.pose_pair.qimage_rgba) - def on_pose_pairs_update(self, number_of_pose_pairs: int): + def on_pose_pairs_update(self, number_of_pose_pairs: int) -> None: self.hand_eye_calibration_buttons.calibrate_button.setEnabled( number_of_pose_pairs >= self.minimum_pose_pairs_for_calibration ) self.pose_pair_selection_widget.setVisible(number_of_pose_pairs > 0) - def process_capture(self, frame: zivid.Frame, rgba: NDArray[Shape["N, M, 4"], UInt8], settings: SettingsPixelMappingIntrinsics): # type: ignore + def process_capture(self, frame: zivid.Frame, rgba: NDArray[Shape["N, M, 4"], UInt8], settings: SettingsPixelMappingIntrinsics) -> None: # type: ignore try: detection_result = ( zivid.calibration.detect_calibration_board(frame) @@ -378,7 +387,7 @@ def process_capture(self, frame: zivid.Frame, rgba: NDArray[Shape["N, M, 4"], UI self.detection_visualization_widget.set_error_message(str(ex)) raise ex - def use_data(self): + def use_data(self) -> None: self.pose_pair_selection_widget.add_pose_pair(self.pose_pair) if self.session_info is not None: save_calibration_config( @@ -393,7 +402,7 @@ def use_data(self): calibrated=False, ) - def on_use_fixed_objects_toggled(self, checked: bool): + def on_use_fixed_objects_toggled(self, checked: bool) -> None: if checked: updated_fixed_objects = set_fixed_objects(self.fixed_objects, self.robot_pose_widget.rotation_information) if updated_fixed_objects is None: @@ -402,7 +411,7 @@ def on_use_fixed_objects_toggled(self, checked: bool): self.hand_eye_calibration_buttons.use_fixed_objects_checkbox.setChecked(self.fixed_objects.has_data()) self.fixed_objects = updated_fixed_objects - def on_calibrate_button_clicked(self): + def on_calibrate_button_clicked(self) -> None: self._calibrate(save_to_disk=True) def _calibrate(self, save_to_disk: bool = True) -> None: @@ -449,26 +458,26 @@ def _calibrate(self, save_to_disk: bool = True) -> None: QMessageBox.critical(self, "Hand-Eye Calibration Error", str(ex)) self.calibration_finished.emit(TransformationMatrix()) - def confirm_robot_pose(self, confirmed: bool = True): + def confirm_robot_pose(self, confirmed: bool = True) -> None: self.update_instructions( has_detection_result=False, robot_pose_confirmed=confirmed, calibrated=False, ) - def on_confirm_robot_pose_button_clicked(self, checked: bool): + def on_confirm_robot_pose_button_clicked(self, checked: bool) -> None: self.confirm_robot_pose(checked) - def on_robot_pose_manually_updated(self): + def on_robot_pose_manually_updated(self) -> None: self.confirm_robot_pose(False) - def on_actual_pose_updated(self, robot_target: RobotTarget): + def on_actual_pose_updated(self, robot_target: RobotTarget) -> None: self.has_detection_result = False self.confirm_robot_pose(True) with QSignalBlocker(self.robot_pose_widget): self.robot_pose_widget.set_transformation_matrix(robot_target.pose) - def on_target_pose_updated(self, robot_target: RobotTarget): + def on_target_pose_updated(self, robot_target: RobotTarget) -> None: self.robot_pose_widget.set_transformation_matrix(robot_target.pose) def get_tab_widgets_in_order(self) -> List[QWidget]: diff --git a/modules/zividsamples/gui/calibration/hand_eye_settings_tester.py b/modules/zividsamples/gui/calibration/hand_eye_settings_tester.py index 0fb38f64..a37d9ea1 100644 --- a/modules/zividsamples/gui/calibration/hand_eye_settings_tester.py +++ b/modules/zividsamples/gui/calibration/hand_eye_settings_tester.py @@ -43,7 +43,7 @@ class CalibrationObjectWidget(QWidget): hand_eye_configuration: HandEyeConfiguration previously_showed_error_message: bool = True - def __init__(self, hand_eye_configuration: HandEyeConfiguration, parent=None): + def __init__(self, hand_eye_configuration: HandEyeConfiguration, parent: Optional[QWidget] = None) -> None: super().__init__(parent) self.hand_eye_configuration = hand_eye_configuration @@ -65,7 +65,7 @@ def __init__(self, hand_eye_configuration: HandEyeConfiguration, parent=None): self.setLayout(layout) self.update_layout() - def update_layout(self): + def update_layout(self) -> None: self.group_box.setTitle(f"{self.hand_eye_configuration.calibration_object.name} in Camera Frame") if self.hand_eye_configuration.calibration_object == CalibrationObject.Markers: self.calibration_object_image.hide() @@ -74,7 +74,7 @@ def update_layout(self): self.markers_widget.hide() self.calibration_object_image.show() - def on_hand_eye_configuration_updated(self, hand_eye_configuration: HandEyeConfiguration): + def on_hand_eye_configuration_updated(self, hand_eye_configuration: HandEyeConfiguration) -> None: self.hand_eye_configuration = hand_eye_configuration self.update_layout() @@ -83,7 +83,7 @@ def set_markers( markers: List[MarkerShape], rgba: NDArray[Shape["N, M, 4"], UInt8], # type: ignore pixel_mapping: PixelMapping, - ): + ) -> None: self.markers_widget.set_markers(markers, rgba, pixel_mapping, reset_zoom=self.previously_showed_error_message) self.previously_showed_error_message = False @@ -99,7 +99,7 @@ def set_checkerboard_image(self, rgba: NDArray[Shape["N, M, 4"], UInt8]) -> None self.calibration_object_image.set_pixmap(qpixmap, reset_zoom=self.previously_showed_error_message) self.previously_showed_error_message = False - def set_error_message(self, error_message: str): + def set_error_message(self, error_message: str) -> None: error_pixmap = QPixmap(400, 400) error_pixmap.fill(Qt.gray) # Set background color painter = QPainter(error_pixmap) @@ -119,7 +119,7 @@ class TestHandEyeCaptureSettings(QMainWindow): hand_eye_configuration: HandEyeConfiguration = HandEyeConfiguration() last_frame: zivid.Frame - def __init__(self, zivid_app: zivid.Application, parent=None): + def __init__(self, zivid_app: zivid.Application, parent: Optional[QWidget] = None) -> None: super().__init__(parent) self.setObjectName("HandEyeSettingsTester") @@ -139,12 +139,12 @@ def __init__(self, zivid_app: zivid.Application, parent=None): self.live2d_widget.start_live_2d() - def configure_settings(self, show_anyway: bool = False): + def configure_settings(self, show_anyway: bool = False) -> None: if self.camera: current_settings = self.settings if hasattr(self, "settings") else None self.settings = select_settings_for_hand_eye(self.camera, current_settings, show_anyway) - def create_widgets(self): + def create_widgets(self) -> None: self.central_widget = QWidget() self.setCentralWidget(self.central_widget) self.calibration_object_widget = CalibrationObjectWidget(hand_eye_configuration=self.hand_eye_configuration) @@ -170,7 +170,7 @@ def create_widgets(self): self.live2d_widget.setFixedHeight(self.calibration_object_widget.height()) self.live2d_widget.setFixedWidth(self.calibration_object_widget.height()) - def setup_layout(self): + def setup_layout(self) -> None: layout = QVBoxLayout(self.central_widget) left_panel = QVBoxLayout() left_panel_buttons = QHBoxLayout() @@ -194,7 +194,7 @@ def setup_layout(self): self.camera_buttons.buttons_layout.addWidget(self.capture_with_hand_eye_settings) layout.addLayout(bottom_layout) - def create_toolbar(self): + def create_toolbar(self) -> None: file_menu = self.menuBar().addMenu("File") self.save_frame_action = QAction("Save", self) self.save_frame_action.triggered.connect(self.on_save_last_frame_action_triggered) @@ -225,7 +225,7 @@ def keyPressEvent(self, a0: QKeyEvent) -> None: # pylint: disable=invalid-name else: super().keyPressEvent(a0) - def connect_signals(self): + def connect_signals(self) -> None: self.camera_buttons.capture_button_clicked.connect(self.on_capture_button_clicked) self.camera_buttons.connect_button_clicked.connect(self.on_connect_button_clicked) self.hand_eye_configuration_buttons.hand_eye_configuration_updated.connect( @@ -233,7 +233,7 @@ def connect_signals(self): ) self.capture_with_hand_eye_settings.toggled.connect(self.on_capture_with_hand_eye_settings_toggled) - def on_capture_button_clicked(self): + def on_capture_button_clicked(self) -> None: assert self.camera is not None self.live2d_widget.stop_live_2d() try: @@ -277,11 +277,11 @@ def on_capture_button_clicked(self): self.calibration_object_widget.set_error_message(f"Failed to capture:\n{ex}") self.live2d_widget.start_live_2d() - def on_hand_eye_configuration_updated(self, hand_eye_configuration: HandEyeConfiguration): + def on_hand_eye_configuration_updated(self, hand_eye_configuration: HandEyeConfiguration) -> None: self.hand_eye_configuration = hand_eye_configuration self.calibration_object_widget.on_hand_eye_configuration_updated(self.hand_eye_configuration) - def on_connect_button_clicked(self): + def on_connect_button_clicked(self) -> None: if self.camera is not None and self.camera.state.connected: self.live2d_widget.stop_live_2d() self.live2d_widget.hide() @@ -300,7 +300,7 @@ def on_connect_button_clicked(self): self.update_live2d_settings() self.live2d_widget.start_live_2d() - def on_capture_with_hand_eye_settings_toggled(self): + def on_capture_with_hand_eye_settings_toggled(self) -> None: if self.camera: if self.capture_with_hand_eye_settings.isChecked(): self.settings_used_in_live_view.setText("Hand Eye Settings (optimized for Hand Eye)") @@ -308,7 +308,7 @@ def on_capture_with_hand_eye_settings_toggled(self): self.settings_used_in_live_view.setText("Production Settings (user choice)") self.update_live2d_settings() - def update_live2d_settings(self): + def update_live2d_settings(self) -> None: if self.camera: if self.capture_with_hand_eye_settings.isChecked(): self.live2d_widget.update_settings_2d( @@ -319,17 +319,17 @@ def update_live2d_settings(self): self.settings.production.settings_2d3d.color, self.camera.info.model ) - def on_select_settings_action_triggered(self): + def on_select_settings_action_triggered(self) -> None: self.configure_settings(show_anyway=True) - def on_save_settings_action_triggered(self): + def on_save_settings_action_triggered(self) -> None: file_name = Path(QFileDialog.getSaveFileName(self, "Save Settings", "", "Zivid Settings (*.yml)")[0]) self.settings.production.settings_2d3d.save(file_name.with_suffix(".yml")) self.settings.hand_eye.settings_2d3d.save( file_name.with_stem(f"{file_name.stem}_for_hand_eye").with_suffix(".yml") ) - def on_visualize_last_frame_action_triggered(self): + def on_visualize_last_frame_action_triggered(self) -> None: point_cloud = self.last_frame.point_cloud() rgba = point_cloud.copy_data("srgb") qimage = QImage( @@ -354,7 +354,7 @@ def on_visualize_last_frame_action_triggered(self): ImageViewerDialog(qimage_depthmap, title="Sample Capture - DepthMap").exec_() display_pointcloud(self.last_frame) - def on_save_last_frame_action_triggered(self): + def on_save_last_frame_action_triggered(self) -> None: if self.last_frame is not None: file_name = QFileDialog.getSaveFileName(self, "Save Capture", "", "Zivid Frame (*.zdf)")[0] self.last_frame.save(file_name) @@ -362,7 +362,7 @@ def on_save_last_frame_action_triggered(self): def log_detection_result( self, detection_result: Union[DetectionResult, DetectionResultFiducialMarkers], - ): + ) -> None: log_message = "" if detection_result.valid(): if self.hand_eye_configuration.calibration_object == CalibrationObject.Checkerboard: diff --git a/modules/zividsamples/gui/calibration/pose_pair_selection_widget.py b/modules/zividsamples/gui/calibration/pose_pair_selection_widget.py index 7e7100f4..37a78695 100644 --- a/modules/zividsamples/gui/calibration/pose_pair_selection_widget.py +++ b/modules/zividsamples/gui/calibration/pose_pair_selection_widget.py @@ -2,7 +2,7 @@ from collections import OrderedDict from dataclasses import dataclass from pathlib import Path -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Optional, Tuple import numpy as np import zivid @@ -15,6 +15,7 @@ QGroupBox, QHBoxLayout, QLabel, + QLayout, QMessageBox, QPushButton, QScrollArea, @@ -81,7 +82,7 @@ def _residual_label_width(label: QLabel) -> int: class ButtonWithLabels(QPushButton): - def __init__(self, labels: List[QLabel], parent=None): + def __init__(self, labels: List[QLabel], parent: Optional[QWidget] = None) -> None: super().__init__(parent) self.labels = labels @@ -113,7 +114,14 @@ class PosePairWidget(QWidget): pose_pair: PosePair # pylint: disable=too-many-positional-arguments - def __init__(self, poseID: int, directory: Path, pose_pair: PosePair, save_to_disk: bool = True, parent=None): + def __init__( + self, + poseID: int, + directory: Path, + pose_pair: PosePair, + save_to_disk: bool = True, + parent: Optional[QWidget] = None, + ) -> None: super().__init__(parent) self.poseID = poseID @@ -148,7 +156,7 @@ def __init__(self, poseID: int, directory: Path, pose_pair: PosePair, save_to_di self.update_information(pose_pair, save_to_disk=save_to_disk) - def update_information(self, pose_pair: PosePair, save_to_disk: bool = True): + def update_information(self, pose_pair: PosePair, save_to_disk: bool = True) -> None: self.pose_pair = pose_pair if save_to_disk: zivid.Matrix4x4(self.pose_pair.robot_pose.as_matrix()).save(self.robot_pose_yaml_path) @@ -182,7 +190,14 @@ class _PosePairLoadWorker(QObject): finished = pyqtSignal(int) # pylint: disable=too-many-positional-arguments - def __init__(self, generation, directory, pose_ids, calibration_object, marker_configuration): + def __init__( + self, + generation: int, + directory: Path, + pose_ids: List[int], + calibration_object: CalibrationObject, + marker_configuration: MarkerConfiguration, + ) -> None: super().__init__() self._generation = generation self._directory = directory @@ -192,10 +207,10 @@ def __init__(self, generation, directory, pose_ids, calibration_object, marker_c self._cancel_event = threading.Event() self._cv2_handler = CV2Handler() - def cancel(self): + def cancel(self) -> None: self._cancel_event.set() - def run(self): + def run(self) -> None: for poseID in self._pose_ids: if self._cancel_event.is_set(): break @@ -260,7 +275,13 @@ class _PosePairReprocessWorker(QObject): pose_pair_reprocessed = pyqtSignal(int, int, object) finished = pyqtSignal(int) - def __init__(self, generation, frames, calibration_object, marker_configuration): + def __init__( + self, + generation: int, + frames: List[Tuple[int, zivid.Frame]], + calibration_object: CalibrationObject, + marker_configuration: MarkerConfiguration, + ) -> None: super().__init__() self._generation = generation self._frames = frames @@ -269,10 +290,10 @@ def __init__(self, generation, frames, calibration_object, marker_configuration) self._cancel_event = threading.Event() self._cv2_handler = CV2Handler() - def cancel(self): + def cancel(self) -> None: self._cancel_event.set() - def run(self): + def run(self) -> None: for poseID, camera_frame in self._frames: if self._cancel_event.is_set(): break @@ -310,7 +331,7 @@ class PosePairSelectionWidget(QWidget): pose_pairs_updated = pyqtSignal(int) loading_finished = pyqtSignal() - def __init__(self, directory: Path, parent=None): + def __init__(self, directory: Path, parent: Optional[QWidget] = None) -> None: super().__init__(parent) self.cv2_handler = CV2Handler() @@ -330,7 +351,7 @@ def __init__(self, directory: Path, parent=None): self.set_directory(directory) - def create_widgets(self): + def create_widgets(self) -> None: self.pose_pair_container = QWidget() self.pose_pairs_group_box = QGroupBox("Pose Pairs") @@ -342,7 +363,7 @@ def create_widgets(self): self.clear_pose_pairs_button = QPushButton("Clear") - def setup_layout(self): + def setup_layout(self) -> None: self.pose_pairs_group_box_layout = QVBoxLayout() self.pose_pairs_group_box_layout.setAlignment(Qt.AlignTop) self.pose_pairs_group_box.setLayout(self.pose_pairs_group_box_layout) @@ -361,13 +382,13 @@ def setup_layout(self): layout.addWidget(self.pose_pairs_group_box) self.setLayout(layout) - def connect_signals(self): + def connect_signals(self) -> None: self.clear_pose_pairs_button.clicked.connect(self.on_clear_button_clicked) - def set_directory(self, directory: Path): + def set_directory(self, directory: Path) -> None: self.directory = directory - def load_pose_pairs(self, calibration_object: CalibrationObject, marker_configuration: MarkerConfiguration): + def load_pose_pairs(self, calibration_object: CalibrationObject, marker_configuration: MarkerConfiguration) -> None: if len(self.pose_pair_widgets) > 0: message_box = QMessageBox() message_box.setText( @@ -424,7 +445,7 @@ def load_pose_pairs(self, calibration_object: CalibrationObject, marker_configur self._loader_worker = worker thread.start() - def cancel_loading(self): + def cancel_loading(self) -> None: if self._loader_worker is not None: self._loader_worker.cancel() if self._loader_thread is not None and self._loader_thread.isRunning(): @@ -434,7 +455,7 @@ def cancel_loading(self): self._loader_thread = None self._cancel_reprocessing() - def _cancel_reprocessing(self): + def _cancel_reprocessing(self) -> None: if self._reprocess_worker is not None: self._reprocess_worker.cancel() if self._reprocess_thread is not None and self._reprocess_thread.isRunning(): @@ -443,7 +464,7 @@ def _cancel_reprocessing(self): self._reprocess_worker = None self._reprocess_thread = None - def _on_pose_pair_loaded(self, generation: int, poseID: int, pose_pair: PosePair): + def _on_pose_pair_loaded(self, generation: int, poseID: int, pose_pair: PosePair) -> None: if generation != self._load_generation: return if poseID in self.pose_pair_widgets: @@ -457,7 +478,7 @@ def _on_pose_pair_loaded(self, generation: int, poseID: int, pose_pair: PosePair self.pose_pairs_updated.emit(len(self.pose_pair_widgets)) self.pose_pair_clicked.emit(pose_pair_widget.pose_pair) - def _on_loading_finished(self, generation: int): + def _on_loading_finished(self, generation: int) -> None: if generation != self._load_generation: return self.pose_pairs_group_box.setStyleSheet("") @@ -517,14 +538,14 @@ def _on_reprocessing_finished(self, generation: int) -> None: self.pose_pairs_group_box.setTitle("Pose Pairs") self.loading_finished.emit() - def on_pose_pair_widget_clicked(self, pose_pair_widget: PosePairWidget): + def on_pose_pair_widget_clicked(self, pose_pair_widget: PosePairWidget) -> None: for clickable_area in [p.clickable_labels for p in self.pose_pair_widgets.values()]: if clickable_area is not pose_pair_widget.clickable_labels: clickable_area.setChecked(False) QApplication.processEvents() self.pose_pair_clicked.emit(pose_pair_widget.pose_pair) - def on_clear_button_clicked(self): + def on_clear_button_clicked(self) -> None: self.clear() def create_title_row(self) -> QHBoxLayout: @@ -554,7 +575,7 @@ def _set_loaded_from_disk(self, value: bool) -> None: self._loaded_from_disk = value self.clear_pose_pairs_button.setVisible(not value) - def add_pose_pair(self, pose_pair) -> Optional[PosePairWidget]: + def add_pose_pair(self, pose_pair: PosePair) -> Optional[PosePairWidget]: if self.is_loading(): return None poseID = self.get_current_poseID() @@ -603,7 +624,7 @@ def get_detection_results(self) -> List[zivid.calibration.HandEyeInput]: if pose_pair_widget.selected_checkbox.isChecked() ] - def set_residuals(self, residuals: List[Any]): + def set_residuals(self, residuals: List[Any]) -> None: checked_pose_pairs = [ pose_pair_widget for pose_pair_widget in self.pose_pair_widgets.values() @@ -621,7 +642,7 @@ def set_residuals(self, residuals: List[Any]): for pose_pair_widget in unchecked_pose_pairs: pose_pair_widget.clickable_labels.labels[2].setText("NA") - def _clear_layout(self, layout): + def _clear_layout(self, layout: QLayout) -> None: while layout.count(): item = layout.takeAt(0) widget = item.widget() @@ -631,7 +652,7 @@ def _clear_layout(self, layout): if sublayout: self._clear_layout(sublayout) - def clear(self): + def clear(self) -> None: self.cancel_loading() self._set_loaded_from_disk(False) self._clear_layout(self.pose_pairs_layout) diff --git a/modules/zividsamples/gui/calibration/set_fixed_objects.py b/modules/zividsamples/gui/calibration/set_fixed_objects.py index 7bfc7f1e..81edf77c 100644 --- a/modules/zividsamples/gui/calibration/set_fixed_objects.py +++ b/modules/zividsamples/gui/calibration/set_fixed_objects.py @@ -49,13 +49,13 @@ class FixedCalibrationObjectsData: calibration_board_pose_eye_to_hand: Optional[TransformationMatrix] = None use_rotation: bool = False - def update_hand_eye_configuration(self, hand_eye_configuration: HandEyeConfiguration): + def update_hand_eye_configuration(self, hand_eye_configuration: HandEyeConfiguration) -> None: self.hand_eye_configuration = hand_eye_configuration if self.hand_eye_configuration.calibration_object == CalibrationObject.Markers: assert self.marker_configuration is not None self.update_marker_configuration(self.marker_configuration) - def update_marker_configuration(self, marker_configuration: MarkerConfiguration): + def update_marker_configuration(self, marker_configuration: MarkerConfiguration) -> None: self.marker_configuration = marker_configuration marker_positions = ( self.marker_positions_eye_in_hand @@ -195,7 +195,7 @@ def __init__(self, fixed_calibration_objects: FixedCalibrationObjectsData): self.add_marker(row, marker_id, [0.0, 0.0, 0.0]) self.setLayout(layout) - def add_marker(self, row: int, marker_id: int, position: List[float]): + def add_marker(self, row: int, marker_id: int, position: List[float]) -> None: marker_with_position = MarkerWithPosition( marker_id=marker_id, position=position, @@ -240,7 +240,7 @@ def __init__( else: self.create_marker_layout() - def create_common_widgets(self): + def create_common_widgets(self) -> None: self.descriptive_text = QLabel() self.fixed_object_pose_eye_to_hand_label = AspectRatioLabel( title="Pose of fixed objects in Eye-to-Hand configuration", @@ -260,7 +260,7 @@ def create_common_widgets(self): def create_checkerboard_widgets( self, initial_rotation_information: RotationInformation, - ): + ) -> None: if self.fixed_calibration_objects_data.hand_eye_configuration.eye_in_hand: self.descriptive_text.setText( ( @@ -298,7 +298,7 @@ def create_checkerboard_widgets( title="Calibration Board Pose", pixmap=QPixmap(get_image_file_path("zvd_cb01_pose.png").as_posix()) ) - def create_marker_widgets(self): + def create_marker_widgets(self) -> None: current_hand_eye_configuration = self.fixed_calibration_objects_data.hand_eye_configuration if current_hand_eye_configuration.eye_in_hand: self.descriptive_text.setText("For each marker, enter its position relative to robot base.") @@ -309,7 +309,7 @@ def create_marker_widgets(self): self.markers_widget = DynamicMarkerList(self.fixed_calibration_objects_data) self.markers_widget.setObjectName("SetFixedObjects-markers_widget") - def create_common_layout(self): + def create_common_layout(self) -> None: descriptive_text_box = QGroupBox("Description") descriptive_text_layout = QVBoxLayout() descriptive_text_layout.addWidget(self.descriptive_text) @@ -325,15 +325,15 @@ def create_common_layout(self): self.horizontal_layout.addWidget(self.fixed_object_pose_eye_to_hand_label) self.setLayout(self.horizontal_layout) - def create_checkerboard_layout(self): + def create_checkerboard_layout(self) -> None: self.left_vertical_layout.addWidget(self.pose_widget) self.left_vertical_layout.addWidget(self.use_rotation_checkbox) self.horizontal_layout.addWidget(self.calibration_board_pose_label) - def create_marker_layout(self): + def create_marker_layout(self) -> None: self.left_vertical_layout.addWidget(self.markers_widget) - def accept(self): + def accept(self) -> None: if ( self.fixed_calibration_objects_data.hand_eye_configuration.calibration_object == CalibrationObject.Checkerboard diff --git a/modules/zividsamples/gui/preparation/infield_correction_data_selection_widget.py b/modules/zividsamples/gui/preparation/infield_correction_data_selection_widget.py index bec8e62c..f87df6b7 100644 --- a/modules/zividsamples/gui/preparation/infield_correction_data_selection_widget.py +++ b/modules/zividsamples/gui/preparation/infield_correction_data_selection_widget.py @@ -2,7 +2,7 @@ import threading from collections import OrderedDict from pathlib import Path -from typing import List, Optional, Union +from typing import List, Optional, Tuple, Union import numpy as np import zivid @@ -17,6 +17,7 @@ QGroupBox, QHBoxLayout, QLabel, + QLayout, QMessageBox, QPushButton, QScrollArea, @@ -39,7 +40,7 @@ def _label_width(label: QLabel, numbers: int) -> int: class ButtonWithLabels(QPushButton): - def __init__(self, labels: List[QLabel], parent=None): + def __init__(self, labels: List[QLabel], parent: Optional[QWidget] = None) -> None: super().__init__(parent) self.labels = labels @@ -83,7 +84,7 @@ def __init__( def local_trueness_as_string(self) -> str: return f"{self.local_trueness * 100:.3f}%" - def save_data(self, data_path) -> None: + def save_data(self, data_path: Path) -> None: with open(data_path.with_suffix(".json"), "w", encoding="utf-8") as f: json.dump( { @@ -101,7 +102,9 @@ def save_data(self, data_path) -> None: qimage_annotated.save(str(data_path.with_suffix(".png")).replace(".png", "_annotated.png")) @classmethod - def from_path(cls, data_path: Path, cv2_handler: CV2Handler) -> "InfieldCorrectionInputDataCore": + def from_path( + cls: type["InfieldCorrectionInputDataCore"], data_path: Path, cv2_handler: CV2Handler + ) -> "InfieldCorrectionInputDataCore": with open(data_path.with_suffix(".json"), "r", encoding="utf-8") as f: data_dict = json.load(f) try: @@ -319,7 +322,7 @@ def fov_points_of_interest_3d(self, camera: zivid.Camera) -> PointsOfInterest: full_fov_corners_in_3d_with_margin = full_fov_corners_in_3d.copy() center = np.array([0, 0, self.fov.distance]) - for index, _ in enumerate(full_fov_corners_in_3d_with_margin): + for index in range(len(full_fov_corners_in_3d_with_margin)): direction = np.sign(full_fov_corners_in_3d[index] - center) full_fov_corners_in_3d_with_margin[index] = ( full_fov_corners_in_3d[index] - FOVThresholds.edge_margin * direction @@ -404,8 +407,8 @@ def fov_points_of_interest_3d(self, camera: zivid.Camera) -> PointsOfInterest: bottom_center_line=np.array(bottom_center_line), ) - def valid_masks(self, points_in_2d, resolution): - non_nan_mask = ~np.isnan(points_in_2d).any(axis=2) + def valid_masks(self, points_in_2d: np.ndarray, resolution: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: + non_nan_mask = np.asarray(~np.isnan(points_in_2d).any(axis=2)) points_in_2d[np.isnan(points_in_2d)] = -1 resolution_mask_camera = np.logical_and( points_in_2d[:, :, 0] >= 0, @@ -420,11 +423,13 @@ def valid_masks(self, points_in_2d, resolution): ) return non_nan_mask, resolution_mask_camera - def valid_mask(self, points_in_2d, resolution): + def valid_mask(self, points_in_2d: np.ndarray, resolution: np.ndarray) -> np.ndarray: non_nan_mask, resolution_mask_camera = self.valid_masks(points_in_2d, resolution) return np.logical_and(non_nan_mask, resolution_mask_camera) - def visualize_masks(self, points_in_2d, resolution, reference_frame="Camera"): + def visualize_masks( + self, points_in_2d: np.ndarray, resolution: np.ndarray, reference_frame: str = "Camera" + ) -> None: non_nan_mask, resolution_mask_camera = self.valid_masks(points_in_2d, resolution) image = np.zeros(non_nan_mask.shape, dtype=np.uint8) image[~non_nan_mask] = 50 @@ -461,11 +466,11 @@ def generate_full_fov_mesh(self, corners: NDArray[Shape["4, 3"], Float32]) -> ND full_fov_points_3d = corners[0] + np.outer(uu.ravel(), vector_x) + np.outer(vv.ravel(), vector_y) return full_fov_points_3d.reshape([uu.shape[0], uu.shape[1], 3]) - def calculate_trapezoidal_corners(self, valid_mask): + def calculate_trapezoidal_corners(self, valid_mask: np.ndarray) -> np.ndarray: valid_mask_cv = valid_mask.astype(np.uint8) - contours, _ = self.cv2_handler.cv2.findContours( + contours = self.cv2_handler.cv2.findContours( valid_mask_cv, self.cv2_handler.cv2.RETR_EXTERNAL, self.cv2_handler.cv2.CHAIN_APPROX_SIMPLE - ) + )[0] contour = max(contours, key=self.cv2_handler.cv2.contourArea) # Approximate to quadrilateral using Ramer–Douglas–Peucker algorithm @@ -498,8 +503,8 @@ def __init__( poseID: int, directory: Path, infield_correction_input_data: Union[InfieldCorrectionInputData, InfieldCorrectionInputDataCore], - parent=None, - ): + parent: Optional[QWidget] = None, + ) -> None: super().__init__(parent) self.poseID = poseID @@ -533,12 +538,12 @@ def __init__( def update_information( self, infield_correction_input_data: Union[InfieldCorrectionInputData, InfieldCorrectionInputDataCore] - ): + ) -> None: self.infield_correction_input_data = infield_correction_input_data self.infield_correction_input_data.save_data(self.directory / f"infield_calibration_input_{self.poseID}") self.update_gui() - def update_gui(self): + def update_gui(self) -> None: self.position_in_fov_label.setText(str(self.infield_correction_input_data.position_in_fov)) self.trueness_label.setText(self.infield_correction_input_data.local_trueness_as_string()) @@ -549,17 +554,17 @@ class _InfieldLoadWorker(QObject): item_loaded = pyqtSignal(int, object) finished = pyqtSignal(int) - def __init__(self, generation, files): + def __init__(self, generation: int, files: List[Path]) -> None: super().__init__() self._generation = generation self._files = files self._cancel_event = threading.Event() self._cv2_handler = CV2Handler() - def cancel(self): + def cancel(self) -> None: self._cancel_event.set() - def run(self): + def run(self) -> None: for file in self._files: if self._cancel_event.is_set(): break @@ -577,7 +582,7 @@ class InfieldCorrectionDataSelectionWidget(QWidget): infield_input_data_updated = pyqtSignal(int) loading_finished = pyqtSignal() - def __init__(self, directory: Path, parent=None): + def __init__(self, directory: Path, parent: Optional[QWidget] = None) -> None: super().__init__(parent) self.cv2_handler = CV2Handler() @@ -591,7 +596,7 @@ def __init__(self, directory: Path, parent=None): self.setup_layout() self.connect_signals() - def create_widgets(self): + def create_widgets(self) -> None: self.infield_input_container = QWidget() self.infield_input_group_box = QGroupBox("Infield Correction Input Data") @@ -605,7 +610,7 @@ def create_widgets(self): self.remove_last_infield_input_button.setEnabled(False) self.clear_infield_input_button = QPushButton("Clear") - def setup_layout(self): + def setup_layout(self) -> None: self.infield_input_group_box_layout = QVBoxLayout() self.infield_input_group_box_layout.setAlignment(Qt.AlignTop) self.infield_input_group_box.setLayout(self.infield_input_group_box_layout) @@ -625,11 +630,11 @@ def setup_layout(self): layout.addWidget(self.infield_input_group_box) self.setLayout(layout) - def connect_signals(self): + def connect_signals(self) -> None: self.clear_infield_input_button.clicked.connect(self.on_clear_button_clicked) self.remove_last_infield_input_button.clicked.connect(self.on_remove_last_infield_input_button_clicked) - def update_data_directory(self, data_directory: Path): + def update_data_directory(self, data_directory: Path) -> None: existing_files = list(data_directory.glob("infield_calibration_input_*.json")) warning_text = ( """\ @@ -657,7 +662,7 @@ def update_data_directory(self, data_directory: Path): if len(existing_files) > 0: self.load_existing_data(existing_files) - def load_existing_data(self, existing_files: List[Path]): + def load_existing_data(self, existing_files: List[Path]) -> None: self.cancel_loading() self._load_generation += 1 @@ -680,7 +685,7 @@ def load_existing_data(self, existing_files: List[Path]): self._loader_worker.finished.connect(self._loader_thread.quit) self._loader_thread.start() - def cancel_loading(self): + def cancel_loading(self) -> None: if self._loader_worker is not None: self._loader_worker.cancel() if self._loader_thread is not None and self._loader_thread.isRunning(): @@ -689,37 +694,39 @@ def cancel_loading(self): self._loader_worker = None self._loader_thread = None - def _on_item_loaded(self, generation: int, data: InfieldCorrectionInputDataCore): + def _on_item_loaded(self, generation: int, data: InfieldCorrectionInputDataCore) -> None: if generation != self._load_generation: return self.add_infield_input_data(data) - def _on_loading_finished(self, generation: int): + def _on_loading_finished(self, generation: int) -> None: if generation != self._load_generation: return self.infield_input_group_box.setStyleSheet("") self.infield_input_group_box.setTitle("Infield Correction Input Data") self.loading_finished.emit() - def on_infield_input_data_widget_selection_box_clicked(self): + def on_infield_input_data_widget_selection_box_clicked(self) -> None: self.infield_input_data_updated.emit(self.can_calculate_correction()) - def on_infield_input_data_widget_clicked(self, infield_correction_input_widget: InfieldCorrectionInputWidget): + def on_infield_input_data_widget_clicked( + self, infield_correction_input_widget: InfieldCorrectionInputWidget + ) -> None: for clickable_area in [p.clickable_labels for p in self.infield_input_data_widgets.values()]: if clickable_area is not infield_correction_input_widget.clickable_labels: clickable_area.setChecked(False) QApplication.processEvents() self.infield_input_data_clicked.emit(infield_correction_input_widget.infield_correction_input_data) - def on_remove_last_infield_input_button_clicked(self): + def on_remove_last_infield_input_button_clicked(self) -> None: if self.infield_input_data_widgets: - _, widget_to_remove = self.infield_input_data_widgets.popitem() + widget_to_remove = self.infield_input_data_widgets.popitem()[1] self.infield_input_layout.removeWidget(widget_to_remove) widget_to_remove.deleteLater() QApplication.processEvents() self.infield_input_data_updated.emit(self.can_calculate_correction()) - def on_clear_button_clicked(self): + def on_clear_button_clicked(self) -> None: self.clear_all() def create_title_row(self) -> QHBoxLayout: @@ -732,7 +739,7 @@ def create_title_row(self) -> QHBoxLayout: title_layout.addItem(remove_button_spacer) return title_layout - def show_as_busy(self, active: bool): + def show_as_busy(self, active: bool) -> None: self.setVisible(active or len(self.infield_input_data_widgets) > 0) self.infield_input_group_box.setStyleSheet(r"QGroupBox {border: 2px solid yellow;}" if active else "") self.infield_input_group_box.setTitle( @@ -812,7 +819,7 @@ def get_correction_results(self) -> List[zivid.calibration.InfieldCorrectionInpu and infield_correction_input_widget.infield_correction_input_data.can_be_used_for_correction ] - def _clear_layout(self, layout): + def _clear_layout(self, layout: QLayout) -> None: while layout.count(): item = layout.takeAt(0) widget = item.widget() @@ -822,7 +829,7 @@ def _clear_layout(self, layout): if sublayout: self._clear_layout(sublayout) - def clear_gui(self): + def clear_gui(self) -> None: self.cancel_loading() self._clear_layout(self.infield_input_layout) self.infield_input_data_widgets.clear() @@ -830,7 +837,7 @@ def clear_gui(self): self.remove_last_infield_input_button.setEnabled(False) self.setVisible(False) - def clear_all(self): + def clear_all(self) -> None: self.clear_gui() if self.infield_input_data_widgets: reply = QMessageBox.question( diff --git a/modules/zividsamples/gui/preparation/infield_correction_gui.py b/modules/zividsamples/gui/preparation/infield_correction_gui.py index cef48c41..9b56d211 100644 --- a/modules/zividsamples/gui/preparation/infield_correction_gui.py +++ b/modules/zividsamples/gui/preparation/infield_correction_gui.py @@ -53,8 +53,8 @@ def __init__( hand_eye_configuration: HandEyeConfiguration, cv2_handler: CV2Handler, get_camera: Callable[[], zivid.Camera], - parent=None, - ): + parent: Optional[QWidget] = None, + ) -> None: super().__init__(data_directory, parent) self.description = [ @@ -83,7 +83,7 @@ def __init__( self.connect_signals() self.update_instructions(has_detection_result=self.has_detection_result, applied_correction=False) - def create_widgets(self): + def create_widgets(self) -> None: self.apply_correction_button = QPushButton("Apply Correction") self.apply_correction_button.setEnabled(False) self.apply_correction_button.setObjectName("InfieldCorrection-apply_correction_button") @@ -98,7 +98,7 @@ def create_widgets(self): self.infield_input_data_selection_widget.setVisible(False) self.infield_correction_result_widget = InfieldCorrectionResultWidget() - def setup_layout(self): + def setup_layout(self) -> None: layout = QVBoxLayout() left_panel = QVBoxLayout() right_panel = QVBoxLayout() @@ -117,14 +117,14 @@ def setup_layout(self): layout.addLayout(bottom_layout) self.setLayout(layout) - def connect_signals(self): + def connect_signals(self) -> None: self.apply_correction_button.clicked.connect(self.on_apply_infield_correction_button_clicked) self.project_fov_hint_button.clicked.connect(self.on_project_fov_hint_button_clicked) self.infield_input_data_selection_widget.infield_input_data_clicked.connect(self.on_infield_input_data_clicked) self.infield_input_data_selection_widget.infield_input_data_updated.connect(self.on_infield_input_data_updated) self.infield_input_data_selection_widget.loading_finished.connect(self.loading_finished) - def update_instructions(self, has_detection_result: bool, applied_correction: bool): + def update_instructions(self, has_detection_result: bool, applied_correction: bool) -> None: self.has_detection_result = has_detection_result self.applied_correction = applied_correction self.instruction_steps = {} @@ -132,31 +132,31 @@ def update_instructions(self, has_detection_result: bool, applied_correction: bo self.instruction_steps["Apply Correction"] = self.applied_correction self.instructions_updated.emit() - def hand_eye_configuration_update(self, hand_eye_configuration: HandEyeConfiguration): + def hand_eye_configuration_update(self, hand_eye_configuration: HandEyeConfiguration) -> None: self.hand_eye_configuration = copy.deepcopy(hand_eye_configuration) self.hand_eye_configuration.calibration_object = CalibrationObject.Checkerboard self.detection_visualization_widget.on_hand_eye_configuration_updated(self.hand_eye_configuration) - def rotation_format_update(self, rotation_information: RotationInformation): + def rotation_format_update(self, rotation_information: RotationInformation) -> None: pass - def robot_configuration_update(self, robot_configuration: RobotConfiguration): + def robot_configuration_update(self, robot_configuration: RobotConfiguration) -> None: self.robot_configuration = robot_configuration self.update_instructions( has_detection_result=self.has_detection_result, applied_correction=self.applied_correction ) - def on_actual_pose_updated(self, robot_target: RobotTarget): + def on_actual_pose_updated(self, robot_target: RobotTarget) -> None: pass def is_loading(self) -> bool: return self.infield_input_data_selection_widget.is_loading() - def on_pending_changes(self): + def on_pending_changes(self) -> None: self.infield_input_data_selection_widget.clear_gui() self.infield_input_data_selection_widget.update_data_directory(self.data_directory) - def on_tab_visibility_changed(self, is_current: bool): + def on_tab_visibility_changed(self, is_current: bool) -> None: pass def on_start_auto_run(self) -> bool: @@ -173,12 +173,12 @@ def on_start_auto_run(self) -> bool: return True return False - def on_infield_input_data_clicked(self, infield_correction_input_data: InfieldCorrectionInputData): + def on_infield_input_data_clicked(self, infield_correction_input_data: InfieldCorrectionInputData) -> None: self.infield_correction_input_data = infield_correction_input_data self.project_fov_hint_button.setEnabled(True) self.detection_visualization_widget.set_rgba_image(self.infield_correction_input_data.rgba_annotated) - def on_infield_input_data_updated(self, can_calculate_correction: bool): + def on_infield_input_data_updated(self, can_calculate_correction: bool) -> None: if can_calculate_correction: correction_results = self.infield_input_data_selection_widget.get_correction_results() self.infield_correction_result_widget.update_result( @@ -192,7 +192,7 @@ def on_infield_input_data_updated(self, can_calculate_correction: bool): self.infield_correction_result_widget.update_result() self.apply_correction_button.setEnabled(can_calculate_correction) - def process_capture(self, frame: zivid.Frame, rgba: NDArray[Shape["N, M, 4"], UInt8], settings: SettingsPixelMappingIntrinsics): # type: ignore + def process_capture(self, frame: zivid.Frame, rgba: NDArray[Shape["N, M, 4"], UInt8], settings: SettingsPixelMappingIntrinsics) -> None: # type: ignore try: detection_result = zivid.calibration.detect_calibration_board(frame) if not detection_result.valid(): @@ -224,7 +224,7 @@ def process_capture(self, frame: zivid.Frame, rgba: NDArray[Shape["N, M, 4"], UI self.on_project_fov_hint_button_clicked() raise ex - def on_apply_infield_correction_button_clicked(self): + def on_apply_infield_correction_button_clicked(self) -> None: if self.applied_correction: self.infield_input_data_selection_widget.clear_gui() self.project_fov_hint_button.setChecked(False) @@ -234,13 +234,13 @@ def on_apply_infield_correction_button_clicked(self): else: self.apply_correction_button_clicked.emit() - def on_project_fov_hint_button_clicked(self): + def on_project_fov_hint_button_clicked(self) -> None: self.project_fov_hint_button.setStyleSheet( "background-color: green;" if self.project_fov_hint_button.isChecked() else "" ) self.update_projection.emit(self.project_fov_hint_button.isChecked()) - def apply_correction(self, camera: zivid.Camera): + def apply_correction(self, camera: zivid.Camera) -> None: try: correction_results = self.infield_input_data_selection_widget.get_correction_results() correction = zivid.calibration.compute_camera_correction(correction_results) @@ -252,7 +252,7 @@ def apply_correction(self, camera: zivid.Camera): print(f"Failed to perform infield correction: {ex}") QMessageBox.critical(self, "Infield Correction Error", str(ex)) - def check_correction(self, camera: zivid.Camera): + def check_correction(self, camera: zivid.Camera) -> None: if zivid.calibration.has_camera_correction(camera): self.infield_correction_result_widget.set_current_correction( zivid.calibration.camera_correction_timestamp(camera) @@ -265,7 +265,7 @@ def has_features_to_project(self) -> bool: self.infield_correction_input_data, InfieldCorrectionInputData ) - def generate_projector_image(self, _: zivid.Camera): + def generate_projector_image(self, _: zivid.Camera) -> NDArray[Shape["N, M, 4"], UInt8]: # type: ignore if not isinstance(self.infield_correction_input_data, InfieldCorrectionInputData): raise RuntimeError("No infield correction input data available to generate projection image.") return self.infield_correction_input_data.projector_image diff --git a/modules/zividsamples/gui/preparation/infield_correction_result_widget.py b/modules/zividsamples/gui/preparation/infield_correction_result_widget.py index 73337a41..42fa1838 100644 --- a/modules/zividsamples/gui/preparation/infield_correction_result_widget.py +++ b/modules/zividsamples/gui/preparation/infield_correction_result_widget.py @@ -8,13 +8,13 @@ class InfieldCorrectionResultWidget(QWidget): - def __init__(self, parent=None): + def __init__(self, parent: Optional[QWidget] = None) -> None: super().__init__(parent) self.create_widgets() self.setup_layout() - def create_widgets(self): + def create_widgets(self) -> None: self.infield_result_container = QWidget() self.infield_result_group_box = QGroupBox("Expected Infield Correction Results") @@ -24,7 +24,7 @@ def create_widgets(self): self.z_max_label = QLabel("NA") self.current_correction_label = QLabel("") - def setup_layout(self): + def setup_layout(self) -> None: infield_correction_result_form = QFormLayout() infield_input_group_box_layout = QHBoxLayout() infield_input_group_box_layout.addLayout(infield_correction_result_form) @@ -41,7 +41,7 @@ def setup_layout(self): layout.addWidget(self.infield_result_group_box) self.setLayout(layout) - def update_result(self, correction: Optional[CameraCorrection] = None): + def update_result(self, correction: Optional[CameraCorrection] = None) -> None: if correction: accuracy_estimate = correction.accuracy_estimate() self.expected_trueness_label.setText(f"{accuracy_estimate.dimension_accuracy() * 100:.3f}%") @@ -52,7 +52,7 @@ def update_result(self, correction: Optional[CameraCorrection] = None): self.z_min_label.setText("NA") self.z_max_label.setText("NA") - def set_current_correction(self, timestamp: Optional[datetime] = None): + def set_current_correction(self, timestamp: Optional[datetime] = None) -> None: if timestamp is None: self.current_correction_label.setText("No correction applied.") else: diff --git a/modules/zividsamples/gui/preparation/warmup_gui.py b/modules/zividsamples/gui/preparation/warmup_gui.py index 37a64453..0a11d001 100644 --- a/modules/zividsamples/gui/preparation/warmup_gui.py +++ b/modules/zividsamples/gui/preparation/warmup_gui.py @@ -14,8 +14,9 @@ import pandas as pd import pyqtgraph as pg import zivid -from nptyping import Float32, NDArray, Shape +from nptyping import Float32, NDArray, Shape, UInt8 from PyQt5.QtCore import Qt, QTimer, pyqtSignal +from PyQt5.QtGui import QKeyEvent from PyQt5.QtWidgets import ( QHBoxLayout, QLabel, @@ -37,22 +38,23 @@ ) from zividsamples.gui.widgets.tab_content_widget import TabContentWidget from zividsamples.gui.wizard.robot_configuration import RobotConfiguration +from zividsamples.gui.wizard.settings_selector import SettingsPixelMappingIntrinsics from zividsamples.paths import get_data_file_path class TimeAxisItem(pg.AxisItem): """Custom axis that formats time in HH:mm:ss.""" - def tickStrings(self, values, _, __): + def tickStrings(self, values: List[float], _: float, __: float) -> List[str]: return [str(timedelta(seconds=int(v))) for v in values] class StepSnappingSlider(QSlider): - def _round_to_step(self, val): + def _round_to_step(self, val: int) -> int: return round(val / self.singleStep()) * self.singleStep() - def keyPressEvent(self, ev): + def keyPressEvent(self, ev: Optional[QKeyEvent]) -> None: if ev: if ev.key() in (Qt.Key_Left, Qt.Key_Right, Qt.Key_PageUp, Qt.Key_PageDown): value = self.value() @@ -96,7 +98,7 @@ def __init__( self.setup_layout() self.connect_signals() - def setup_widgets(self, initial_min_val: int, initial_max_val: int): + def setup_widgets(self, initial_min_val: int, initial_max_val: int) -> None: self.min_slider = StepSnappingSlider(Qt.Horizontal) self.min_val_input = QLineEdit() self.max_slider = StepSnappingSlider(Qt.Horizontal) @@ -111,7 +113,7 @@ def setup_widgets(self, initial_min_val: int, initial_max_val: int): self.update_text() - def setup_layout(self): + def setup_layout(self) -> None: top_layout = QHBoxLayout() bottom_layout = QHBoxLayout() layout = QVBoxLayout() @@ -125,17 +127,17 @@ def setup_layout(self): layout.addLayout(bottom_layout) self.setLayout(layout) - def connect_signals(self): + def connect_signals(self) -> None: self.min_slider.valueChanged.connect(self.slider_min_val_changed) self.max_slider.valueChanged.connect(self.slider_max_val_changed) self.min_val_input.editingFinished.connect(self.min_val_input_changed) self.max_val_input.editingFinished.connect(self.max_val_input_changed) - def update_text(self): + def update_text(self) -> None: self.min_val_input.setText(f"{self.min_slider.value():6.0f} ms") self.max_val_input.setText(f"{self.max_slider.value():6.0f} ms") - def slider_min_val_changed(self): + def slider_min_val_changed(self) -> None: min_val = self.min_slider.value() if min_val > self.max_slider.value(): @@ -145,7 +147,7 @@ def slider_min_val_changed(self): self.rangeChanged.emit(self.min_slider.value(), self.max_slider.value()) - def slider_max_val_changed(self): + def slider_max_val_changed(self) -> None: max_val = self.max_slider.value() if max_val < self.min_slider.value(): @@ -155,7 +157,7 @@ def slider_max_val_changed(self): self.rangeChanged.emit(self.min_slider.value(), self.max_slider.value()) - def min_val_input_changed(self): + def min_val_input_changed(self) -> None: try: min_val = int(self.min_val_input.text().strip("ms")) if min_val < self.range_min: @@ -166,7 +168,7 @@ def min_val_input_changed(self): except ValueError: self.update_text() - def max_val_input_changed(self): + def max_val_input_changed(self) -> None: try: max_val = int(self.max_val_input.text().strip("ms")) if max_val < self.range_min: @@ -213,7 +215,7 @@ def add_data( temperature: float, local_trueness: Optional[float], distance: Optional[float], - ): + ) -> None: if self.serial_number is None: self.serial_number = serial_number elif self.serial_number != serial_number: @@ -236,7 +238,7 @@ def add_data( self.df = pd.concat([self.df, new_entry_df], ignore_index=True) self._update_rate_of_change() - def _update_rate_of_change(self): + def _update_rate_of_change(self) -> None: # Ensure enough data and window size is odd if len(self.df) < self.window_size or self.window_size % 2 == 0: return @@ -326,11 +328,11 @@ def has_enough_data_to_plot(self) -> bool: def has_enough_data_to_analyze(self) -> bool: return len(self.df) > self.window_size - def clear(self): + def clear(self) -> None: self.df = self._empty_df_template.copy() self.serial_number = None - def write_to_csv(self): + def write_to_csv(self) -> None: self.df.to_csv( f"camera_{self.serial_number}_warmup_{datetime.now().isoformat('_', 'seconds').replace(':','-')}.csv", index=False, @@ -347,7 +349,7 @@ class WarmUpGUI(TabContentWidget): instruction_steps: Dict[str, bool] # pylint: disable=too-many-positional-arguments - def __init__(self, data_directory: Path, parent=None): + def __init__(self, data_directory: Path, parent: Optional[QWidget] = None) -> None: super().__init__(data_directory, parent) temperature_change_threshold_degrees_per_minute = 2.0 @@ -382,7 +384,7 @@ def __init__(self, data_directory: Path, parent=None): ready=self.ready_to_start, started=self.started, equilibrium_reached=self.equilibrium_reached ) - def create_widgets(self): + def create_widgets(self) -> None: self.warmup_button = QPushButton("Start Warmup") self.warmup_button.setObjectName("Warmup-start_warmup_button") self.capture_interval_slider = QRangeSlider( @@ -430,7 +432,7 @@ def create_widgets(self): ) ) - def setup_layout(self): + def setup_layout(self) -> None: layout = QVBoxLayout() top_panel = QVBoxLayout() middle_panel = QHBoxLayout() @@ -445,13 +447,13 @@ def setup_layout(self): layout.addLayout(bottom_layout) self.setLayout(layout) - def connect_signals(self): + def connect_signals(self) -> None: self.warmup_button.clicked.connect(self.on_warmup_button_clicked) self.capture_interval_slider.rangeChanged.connect(self.update_capture_interval) self.update_graph_signal.connect(self.update_graph) self.information_updated.connect(self.update_information_text) - def update_instructions(self, ready: bool, started: bool, equilibrium_reached: bool): + def update_instructions(self, ready: bool, started: bool, equilibrium_reached: bool) -> None: self.ready_to_start = ready self.started = started self.equilibrium_reached = equilibrium_reached @@ -461,17 +463,17 @@ def update_instructions(self, ready: bool, started: bool, equilibrium_reached: b self.instruction_steps["Warmup Completed"] = self.equilibrium_reached self.instructions_updated.emit() - def on_pending_changes(self): + def on_pending_changes(self) -> None: pass - def on_tab_visibility_changed(self, is_current: bool): + def on_tab_visibility_changed(self, is_current: bool) -> None: if not is_current: self.stop_warmup() - def robot_configuration_update(self, _: RobotConfiguration): + def robot_configuration_update(self, _: RobotConfiguration) -> None: return - def update_information_text(self): + def update_information_text(self) -> None: if self.last_local_trueness_measurement is not None: self.information_area.clear() text = "" @@ -491,12 +493,12 @@ def update_information_text(self): self.information_area.updateGeometry() self.information_area.repaint() - def update_capture_interval(self, min_val, max_val): + def update_capture_interval(self, min_val: int, max_val: int) -> None: self.random_capture_cycle.min_capture_interval = timedelta(seconds=min_val / 1000) self.random_capture_cycle.max_capture_interval = timedelta(seconds=max_val / 1000) self.information_updated.emit() - def on_warmup_button_clicked(self): + def on_warmup_button_clicked(self) -> None: if self.started: self.stop_warmup() else: @@ -515,13 +517,13 @@ def capture_for_warmup( info=camera.info, ) - def stop_warmup(self): + def stop_warmup(self) -> None: if self.started: self.stop_requested = True while self.stop_requested: time.sleep(0.1) - def start_warmup(self, camera: zivid.Camera, production_settings: zivid.Settings): + def start_warmup(self, camera: zivid.Camera, production_settings: zivid.Settings) -> None: try: self.warmup_button.setText("Stop Warmup") self.warmup_button.setStyleSheet("background-color: yellow; color: black;") @@ -553,7 +555,7 @@ def start_warmup(self, camera: zivid.Camera, production_settings: zivid.Settings self.warmup_button.setStyleSheet("") self.end_warmup(equilibrium_reached=False) - def warmup_cycle(self, camera: zivid.Camera, production_settings: zivid.Settings): + def warmup_cycle(self, camera: zivid.Camera, production_settings: zivid.Settings) -> None: self.update_instructions(ready=self.ready_to_start, started=True, equilibrium_reached=self.equilibrium_reached) trueness_measurement_interval = timedelta( seconds=max(20, self.random_capture_cycle.max_capture_interval.total_seconds()) @@ -614,7 +616,7 @@ def warmup_cycle(self, camera: zivid.Camera, production_settings: zivid.Settings time.sleep(2) self.end_warmup(self.equilibrium_reached) - def end_warmup(self, equilibrium_reached: bool): + def end_warmup(self, equilibrium_reached: bool) -> None: self.started = False self.stop_requested = False self.equilibrium_reached = equilibrium_reached @@ -630,7 +632,7 @@ def end_warmup(self, equilibrium_reached: bool): self.full_history_of_results.write_to_csv() self.full_history_of_results.clear() - def approximate_value_at_distance(self, camera: zivid.Camera, distance: float): + def approximate_value_at_distance(self, camera: zivid.Camera, distance: float) -> float: approx_value = 0 values_a = self.camera_parameters.loc[camera.info.model_name]["key_c"] for i, val_a in enumerate(values_a): @@ -651,16 +653,16 @@ def get_warn_about_trueness_str(self, camera: zivid.Camera) -> str: We recommend that you perform Infield Correction.""" return "" - def clear_plots(self): + def clear_plots(self) -> None: self.plot_data_item_temperature.clear() self.plot_data_item_temperature_rate_of_change.clear() - def update_graph(self): + def update_graph(self) -> None: - def round_down_to_nearest_5(x): + def round_down_to_nearest_5(x: float) -> float: return 5 * np.floor(x / 5) - def round_up_to_nearest_5(x): + def round_up_to_nearest_5(x: float) -> float: return 5 * np.ceil(x / 5) if self.full_history_of_results.has_enough_data_to_plot(): @@ -680,7 +682,7 @@ def round_up_to_nearest_5(x): ) self.rate_of_change_plot.setYRange(0.01, np.nanmax(rate_of_change_data) * 1.1) - def process_capture(self, frame: zivid.Frame, _, __): # type: ignore + def process_capture(self, frame: zivid.Frame, _: NDArray[Shape["N, M, 4"], UInt8], __: SettingsPixelMappingIntrinsics) -> None: # type: ignore if not self.started: try: test_result = capture_and_measure_from_frame(frame) diff --git a/modules/zividsamples/gui/qt_application.py b/modules/zividsamples/gui/qt_application.py index f991c6e8..b2b38b07 100644 --- a/modules/zividsamples/gui/qt_application.py +++ b/modules/zividsamples/gui/qt_application.py @@ -3,10 +3,11 @@ import sys from dataclasses import dataclass from pathlib import Path -from typing import Tuple +from types import TracebackType +from typing import Optional, Tuple, Type from PyQt5.QtGui import QColor, QFont, QIcon -from PyQt5.QtWidgets import QApplication, QDesktopWidget, QFrame +from PyQt5.QtWidgets import QApplication, QDesktopWidget, QFrame, QWidget from zividsamples.paths import get_image_file_path os.environ["QT_ENABLE_HIGHDPI_SCALING"] = "1" @@ -295,7 +296,7 @@ def __init__(self, use_zivid_app: bool = True): self.zivid_app = zivid.Application() - def run(self, win, title: str = "Zivid Qt Application"): + def run(self, win: QWidget, title: str = "Zivid Qt Application") -> int: icon_path = get_image_file_path("LogoZBlue.ico") self.setWindowIcon(QIcon(icon_path.absolute().as_posix())) if sys.platform == "win32": @@ -318,5 +319,10 @@ def run(self, win, title: str = "Zivid Qt Application"): def __enter__(self): return self - def __exit__(self, exception_type, exception_value, traceback): + def __exit__( + self, + exception_type: Optional[Type[BaseException]], + exception_value: Optional[BaseException], + traceback: Optional[TracebackType], + ) -> None: pass diff --git a/modules/zividsamples/gui/robot/robot_control.py b/modules/zividsamples/gui/robot/robot_control.py index 92e91fcd..dd790c37 100644 --- a/modules/zividsamples/gui/robot/robot_control.py +++ b/modules/zividsamples/gui/robot/robot_control.py @@ -25,11 +25,11 @@ def get_pose(self) -> RobotTarget: raise NotImplementedError @abstractmethod - def connect(self): + def connect(self) -> None: raise NotImplementedError @abstractmethod - def disconnect(self): + def disconnect(self) -> None: raise NotImplementedError def is_read_only(self) -> bool: diff --git a/modules/zividsamples/gui/robot/robot_control_robodk.py b/modules/zividsamples/gui/robot/robot_control_robodk.py index 13c57579..0d25f981 100644 --- a/modules/zividsamples/gui/robot/robot_control_robodk.py +++ b/modules/zividsamples/gui/robot/robot_control_robodk.py @@ -1,7 +1,7 @@ import time from datetime import datetime, timedelta from pathlib import Path -from typing import Any, Dict, List, Optional +from typing import Any, Callable, Dict, List, Optional, TypeVar import numpy as np from PyQt5.QtCore import pyqtSignal @@ -12,6 +12,8 @@ from zividsamples.gui.wizard.robot_configuration import RobotConfiguration from zividsamples.transformation_matrix import Distance, TransformationMatrix +T = TypeVar("T") + class RobotControlRoboDK(RobotControl): rdk: Optional[Robolink] @@ -31,7 +33,7 @@ def __init__(self, robot_configuration: RobotConfiguration): super().__init__(robot_configuration=robot_configuration) self.robot_handle = None - def _robodk_pose_to_transformation_matrix(self, pose: Any) -> TransformationMatrix: + def _robodk_pose_to_transformation_matrix(self, pose: Mat) -> TransformationMatrix: matrix = np.asarray(pose).transpose() return TransformationMatrix.from_matrix(matrix) @@ -96,11 +98,11 @@ def get_number_of_regular_targets(self) -> int: def is_moving(self) -> bool: return self.robot_moving - def disconnect(self): + def disconnect(self) -> None: if self.rdk is not None: self.rdk.Disconnect() - def setup_station(self): + def setup_station(self) -> None: assert self.rdk active_station = self.rdk.ActiveStation() print(f"Currently active station: {active_station.Name()}") @@ -127,14 +129,14 @@ def setup_station(self): )[0] self.rdk.Save(station_backup_path) self.rdk.CloseStation() - station_path, _ = QFileDialog.getOpenFileName( + station_path = QFileDialog.getOpenFileName( None, "Select Station", directory=Path.home().resolve().as_posix(), filter="RoboDK Station (*.rdk)" - ) + )[0] self.rdk.AddFile(station_path) active_station = self.rdk.ActiveStation() print(f"Chosen station: {active_station.Name()}") - def setup_targets(self): + def setup_targets(self) -> None: assert self.rdk list_items = self.rdk.ItemList() robodk_items = {item.Name(): item for item in list_items} @@ -168,7 +170,7 @@ def setup_targets(self): ) self.custom_target = robodk_items["CustomTarget"] - def connect(self): + def connect(self) -> None: self.rdk = Robolink(args=["/NOSPLASH", "/NOSHOW", "/HIDDEN"], quit_on_close=True) if self.rdk is None: raise RuntimeError("Robolink failed to initialize") @@ -196,7 +198,7 @@ def connect(self): self.robot_handle.setAcceleration(50) self.robot_handle.setAccelerationJoints(50) - def retry_function_call(self, function_to_call, *args, **kwargs): + def retry_function_call(self, function_to_call: Callable[..., T], *args: object, **kwargs: object) -> Optional[T]: max_retries = 3 delay = 0.25 for attempt in range(max_retries): diff --git a/modules/zividsamples/gui/robot/robot_control_ur_rtde_read_only.py b/modules/zividsamples/gui/robot/robot_control_ur_rtde_read_only.py index b1adb3e9..24c5cc04 100644 --- a/modules/zividsamples/gui/robot/robot_control_ur_rtde_read_only.py +++ b/modules/zividsamples/gui/robot/robot_control_ur_rtde_read_only.py @@ -30,7 +30,7 @@ def get_pose(self) -> RobotTarget: if self.robot_handle is None: raise RuntimeError("RTDE interface not connected.") try: - for _ in range(5): + for _ in range(5): # pylint: disable=unused-variable,redefined-outer-name if self.robot_handle.has_data(): tcp_pose = self.robot_handle.receive().actual_TCP_pose return RobotTarget( @@ -46,7 +46,7 @@ def get_pose(self) -> RobotTarget: raise RuntimeError(f"Error while receiving RTDE data: {e}") from e raise RuntimeError("No RTDE data received from robot.") - def disconnect(self): + def disconnect(self) -> None: if self.robot_handle is not None: try: start_time = datetime.now() @@ -63,7 +63,7 @@ def disconnect(self): print(f"Error while disconnecting RTDE: {e}") self.robot_handle = None - def connect(self): + def connect(self) -> None: # Use NamedTemporaryFile with delete=False and mode='w+t' for Windows compatibility with tempfile.NamedTemporaryFile(mode="w+t", delete=False, suffix=".xml") as temp_file: temp_file.write(UR_RTDE_RECIPE) diff --git a/modules/zividsamples/gui/robot/robot_control_widget.py b/modules/zividsamples/gui/robot/robot_control_widget.py index bccd27e7..d02d2f53 100644 --- a/modules/zividsamples/gui/robot/robot_control_widget.py +++ b/modules/zividsamples/gui/robot/robot_control_widget.py @@ -43,8 +43,8 @@ def __init__( self, get_user_pose: Callable[[], TransformationMatrix], robot_configuration: RobotConfiguration, - parent=None, - ): + parent: Optional[QWidget] = None, + ) -> None: super().__init__(parent) self.get_user_pose: Callable[[], TransformationMatrix] = get_user_pose @@ -65,7 +65,7 @@ def __init__( self.setup_layout() self.connect_signals() - def create_widgets(self): + def create_widgets(self) -> None: self.ip_input = QLineEdit() self.ip_input.setText(self.robot_configuration.ip_addr) @@ -117,11 +117,11 @@ def create_widgets(self): self.btn_move_to_next_target.setVisible(self.robot_configuration.can_control()) self.btn_auto_run.setVisible(self.robot_configuration.can_control()) - def start_get_pose_timer(self): + def start_get_pose_timer(self) -> None: self.get_pose_timer = threading.Timer(self.get_pose_interval_ms / 1000.0, self.on_get_pose) self.get_pose_timer.start() - def setup_layout(self): + def setup_layout(self) -> None: ip_form = QFormLayout() ip_form.addRow("Robot IP", self.ip_input) @@ -141,7 +141,7 @@ def setup_layout(self): layout.addWidget(self.robot_control_group_box) self.setLayout(layout) - def connect_signals(self): + def connect_signals(self) -> None: self.ip_input.editingFinished.connect(self.on_ip_input_editing_finished) self.btn_connect_robot.clicked.connect(self.on_connect_to_robot) self.btn_move_to_next_target.clicked.connect(self.on_move_to_next_target) @@ -154,7 +154,7 @@ def connect_signals(self): if self.robot_control and self.robot_configuration.can_control(): self.robot_control.target_pose_updated.connect(self.target_pose_updated.emit) - def robot_configuration_update(self, robot_configuration: RobotConfiguration): + def robot_configuration_update(self, robot_configuration: RobotConfiguration) -> None: self.robot_configuration = robot_configuration self.setVisible(self.robot_configuration.has_robot()) self.ip_input.setText(self.robot_configuration.ip_addr) @@ -178,7 +178,7 @@ def robot_configuration_update(self, robot_configuration: RobotConfiguration): self.robot_control.target_pose_updated.connect(self.target_pose_updated.emit) self.btn_move_to_current_target.setVisible(self.robot_configuration.allow_unsafe_move) - def enable_disable_buttons(self, auto_run: bool, touch: bool): + def enable_disable_buttons(self, auto_run: bool, touch: bool) -> None: if self.robot_control and self.robot_control.is_read_only(): return self.buttons_which_depend_on_connection_status[self.btn_auto_run] = auto_run @@ -188,12 +188,12 @@ def enable_disable_buttons(self, auto_run: bool, touch: bool): for button, status in self.buttons_which_depend_on_connection_status.items(): button.setEnabled(status) - def show_buttons(self, auto_run: bool, touch: bool): + def show_buttons(self, auto_run: bool, touch: bool) -> None: if self.robot_configuration.can_control(): self.btn_auto_run.setVisible(auto_run) self.btn_move_to_touch_target.setVisible(touch) - def set_auto_run_active(self, auto_run_active: bool): + def set_auto_run_active(self, auto_run_active: bool) -> None: self.btn_auto_run.setChecked(auto_run_active) for button, status in self.buttons_which_depend_on_connection_status.items(): if button != self.btn_auto_run: @@ -205,22 +205,22 @@ def set_auto_run_active(self, auto_run_active: bool): self.btn_auto_run.setStyleSheet("") self.btn_auto_run.setText("Auto Run") - def on_auto_run_toggled(self): + def on_auto_run_toggled(self) -> None: self.auto_run_toggled.emit() - def robot_is_moving(self): + def robot_is_moving(self) -> bool: if self.robot_configuration.can_control(): assert self.robot_control is not None return self.robot_control.is_moving() return False - def robot_is_home(self): + def robot_is_home(self) -> bool: if self.robot_configuration.can_control(): assert self.robot_control is not None return self.robot_control.is_home() return False - def on_robot_done_moving(self): + def on_robot_done_moving(self) -> None: if self.result_queue is None: raise RuntimeError("result_queue is not initialized") if self.activeButton is None: @@ -233,7 +233,7 @@ def on_robot_done_moving(self): if not result: print("Failed to `move_to_current_target()`") - def on_get_pose(self): + def on_get_pose(self) -> None: assert self.robot_control is not None try: actual_pose = self.robot_control.get_pose() @@ -247,7 +247,7 @@ def on_get_pose(self): if self.robot_control.is_read_only(): self.start_get_pose_timer() - def set_get_pose_interval(self, fast: bool): + def set_get_pose_interval(self, fast: bool) -> None: if fast: self.get_pose_interval_ms = GET_POSE_INTERVAL_FAST_MS else: @@ -257,7 +257,7 @@ def set_get_pose_interval(self, fast: bool): self.get_pose_timer = None self.start_get_pose_timer() - def disconnect(self): + def disconnect(self) -> None: if self.connected: assert self.robot_control is not None if self.get_pose_timer is not None: @@ -273,7 +273,7 @@ def disconnect(self): self.connected = False self.robot_connected.emit(False) - def on_connect_to_robot(self): + def on_connect_to_robot(self) -> None: assert self.robot_control is not None if self.connected: self.disconnect() @@ -319,7 +319,7 @@ def move_robot_in_separate_thread(self, emit_signal_when_done: bool = False, mov elif self.activeButton is not None: print(f"Robot finished a blocking move, button: {self.activeButton.objectName()}") - def on_robot_move_error(self, error_message: str): + def on_robot_move_error(self, error_message: str) -> None: if self.activeButton is not None: self.activeButton.setChecked(False) self.activeButton.setStyleSheet("") @@ -349,7 +349,7 @@ def move_to_current_target(self, blocking: bool = True, moveL: bool = False) -> print("Failed to `move_to_current_target()`") return result - def on_move_home(self): + def on_move_home(self) -> None: assert self.robot_control is not None self.target_counter = 0 self.current_robot_target = self.robot_control.get_target_by_id(self.target_counter) @@ -357,7 +357,7 @@ def on_move_home(self): if self.move_to_current_target(): self.btn_move_home.setStyleSheet("background-color: green;") - def on_move_to_next_target(self, blocking: bool = True): + def on_move_to_next_target(self, blocking: bool = True) -> None: assert self.robot_control is not None self.target_counter += 1 if self.target_counter == self.robot_control.get_number_of_regular_targets(): @@ -370,7 +370,7 @@ def on_move_to_next_target(self, blocking: bool = True): self.activeButton = self.btn_move_to_next_target self.move_to_current_target(blocking) - def on_move_to_touch_target(self): + def on_move_to_touch_target(self) -> None: assert self.robot_control is not None if self.touch_target is None: QMessageBox.warning(self, "Robot Control", "Aruco target not acquired") @@ -396,18 +396,18 @@ def on_move_to_touch_target(self): self.current_robot_target = self.robot_control.get_safe_waypoint() self.move_to_current_target() - def on_move_to_current_target(self): + def on_move_to_current_target(self) -> None: assert self.robot_control is not None self.current_robot_target = self.robot_control.get_custom_target(self.get_user_pose()) self.activeButton = self.btn_move_to_current_target self.move_to_current_target() self.btn_move_home.setStyleSheet("") - def set_touch_target(self, touch_target: TransformationMatrix): + def set_touch_target(self, touch_target: TransformationMatrix) -> None: self.touch_target = touch_target self.btn_move_to_touch_target.setEnabled(True) - def on_ip_input_editing_finished(self): + def on_ip_input_editing_finished(self) -> None: if self.connected: disconnect = QMessageBox.question( self, diff --git a/modules/zividsamples/gui/verification/capture_at_pose_selection_widget.py b/modules/zividsamples/gui/verification/capture_at_pose_selection_widget.py index 72b0593d..19eacac7 100644 --- a/modules/zividsamples/gui/verification/capture_at_pose_selection_widget.py +++ b/modules/zividsamples/gui/verification/capture_at_pose_selection_widget.py @@ -1,7 +1,7 @@ import threading from collections import OrderedDict from pathlib import Path -from typing import Optional +from typing import List, Optional import numpy as np import zivid @@ -13,6 +13,7 @@ QGroupBox, QHBoxLayout, QLabel, + QLayout, QMessageBox, QPushButton, QSizePolicy, @@ -108,7 +109,7 @@ def __init__( QApplication.instance().style().standardIcon(QStyle.SP_DialogDiscardButton) ) - def save_as_ply(self): + def save_as_ply(self) -> None: self.camera_frame.save(self.directory / f"capture_{self.poseID}.ply") def robot_pose_yaml_text(self) -> str: @@ -122,7 +123,14 @@ class _CaptureAtPoseLoadWorker(QObject): finished = pyqtSignal(int) # pylint: disable=too-many-positional-arguments - def __init__(self, generation, directory, pose_ids, hand_eye_transform, eye_in_hand): + def __init__( + self, + generation: int, + directory: Path, + pose_ids: List[int], + hand_eye_transform: TransformationMatrix, + eye_in_hand: bool, + ) -> None: super().__init__() self._generation = generation self._directory = directory @@ -131,10 +139,10 @@ def __init__(self, generation, directory, pose_ids, hand_eye_transform, eye_in_h self._eye_in_hand = eye_in_hand self._cancel_event = threading.Event() - def cancel(self): + def cancel(self) -> None: self._cancel_event.set() - def run(self): + def run(self) -> None: for poseID in self._pose_ids: if self._cancel_event.is_set(): break @@ -168,7 +176,7 @@ class CaptureAtPoseSelectionWidget(QWidget): selected_captures_updated = pyqtSignal() loading_finished = pyqtSignal() - def __init__(self, directory: Path, parent=None): + def __init__(self, directory: Path, parent: Optional[QWidget] = None) -> None: super().__init__(parent) self.directory = directory @@ -199,10 +207,10 @@ def __init__(self, directory: Path, parent=None): self.clear_button.clicked.connect(self.on_clear_button_clicked) - def set_directory(self, directory: Path): + def set_directory(self, directory: Path) -> None: self.directory = directory - def load_capture_at_poses(self, hand_eye_transform: TransformationMatrix, eye_in_hand: bool): + def load_capture_at_poses(self, hand_eye_transform: TransformationMatrix, eye_in_hand: bool) -> None: if self.number_of_active_captures() > 0: reply = QMessageBox.question( self, @@ -246,7 +254,7 @@ def load_capture_at_poses(self, hand_eye_transform: TransformationMatrix, eye_in self._loader_worker.finished.connect(self._loader_thread.quit) self._loader_thread.start() - def cancel_loading(self): + def cancel_loading(self) -> None: if self._loader_worker is not None: self._loader_worker.cancel() if self._loader_thread is not None and self._loader_thread.isRunning(): @@ -257,7 +265,7 @@ def cancel_loading(self): def _on_capture_loaded( self, generation: int, poseID: int, robot_pose: TransformationMatrix, camera_frame: zivid.Frame - ): + ) -> None: if generation != self._load_generation: return if poseID in self.capture_at_poses: @@ -288,27 +296,27 @@ def _on_capture_loaded( self.capture_at_poses[poseID] = capture_at_pose self.clear_button.setEnabled(True) - def _on_loading_finished(self, generation: int): + def _on_loading_finished(self, generation: int) -> None: if generation != self._load_generation: return self.captures_group_box.setTitle("Captures") self.loading_finished.emit() - def get_selected_capture_at_poses(self): + def get_selected_capture_at_poses(self) -> List[CaptureAtPose]: return [pose_pair for pose_pair in self.capture_at_poses.values() if pose_pair.selected_checkbox.isChecked()] - def on_capture_at_pose_clicked(self, capture_at_pose: CaptureAtPose): + def on_capture_at_pose_clicked(self, capture_at_pose: CaptureAtPose) -> None: for capture_pose_button in [p.capture_pose_button for p in self.capture_at_poses.values()]: if capture_pose_button is not capture_at_pose.capture_pose_button: capture_pose_button.setChecked(False) QApplication.processEvents() self.capture_at_pose_clicked.emit(capture_at_pose) - def on_clear_button_clicked(self): + def on_clear_button_clicked(self) -> None: self.clear() self.clear_button.setEnabled(False) - def remove_capture_at_pose(self, capture_at_pose: CaptureAtPose): + def remove_capture_at_pose(self, capture_at_pose: CaptureAtPose) -> None: del self.capture_at_poses[capture_at_pose.poseID] self._clear_layout(self.captures_layout.itemAt(capture_at_pose.poseID)) @@ -321,7 +329,7 @@ def add_capture_at_pose( camera_frame: zivid.Frame, hand_eye_transform: TransformationMatrix, eye_in_hand: bool, - ): + ) -> None: if self.is_loading(): return poseID = self.get_current_poseID() @@ -377,7 +385,7 @@ def number_of_active_captures(self) -> int: [pose_pair for pose_pair in self.capture_at_poses.values() if pose_pair.selected_checkbox.isChecked()] ) - def _clear_layout(self, layout): + def _clear_layout(self, layout: QLayout) -> None: while layout.count(): item = layout.takeAt(0) widget = item.widget() @@ -387,7 +395,7 @@ def _clear_layout(self, layout): if sublayout: self._clear_layout(sublayout) - def clear(self): + def clear(self) -> None: self.cancel_loading() self._clear_layout(self.captures_layout) self.capture_at_poses.clear() diff --git a/modules/zividsamples/gui/verification/hand_eye_verification_gui.py b/modules/zividsamples/gui/verification/hand_eye_verification_gui.py index 2768f122..cf38c0a3 100644 --- a/modules/zividsamples/gui/verification/hand_eye_verification_gui.py +++ b/modules/zividsamples/gui/verification/hand_eye_verification_gui.py @@ -7,7 +7,7 @@ """ from pathlib import Path -from typing import Dict, List +from typing import Dict, List, Optional import numpy as np import zivid @@ -69,8 +69,8 @@ def __init__( marker_configuration: MarkerConfiguration, cv2_handler: CV2Handler, initial_rotation_information: RotationInformation, - parent=None, - ): + parent: Optional[QWidget] = None, + ) -> None: super().__init__(data_directory, parent) self.description = [ @@ -94,7 +94,7 @@ def __init__( self.connect_signals() self.update_instructions(has_set_object_poses_in_robot_frame=False, robot_pose_confirmed=False) - def create_widgets(self, initial_rotation_information: RotationInformation): + def create_widgets(self, initial_rotation_information: RotationInformation) -> None: self.robot_pose_widget = PoseWidget.Robot( eye_in_hand=self.hand_eye_configuration.eye_in_hand, display_mode=PoseWidgetDisplayMode.OnlyPose, @@ -139,7 +139,7 @@ def create_widgets(self, initial_rotation_information: RotationInformation): self.detection_visualization_widget = DetectionVisualizationWidget(self.hand_eye_configuration) self.detection_visualization_widget.descriptive_image_label.hide() - def setup_layout(self): + def setup_layout(self) -> None: layout = QVBoxLayout() self.calibration_object_poses_layout = QVBoxLayout() @@ -160,11 +160,11 @@ def setup_layout(self): self.setLayout(layout) - def connect_signals(self): + def connect_signals(self) -> None: self.confirm_robot_pose_button.clicked.connect(self.on_confirm_robot_pose_button_clicked) self.robot_pose_widget.pose_updated.connect(self.on_robot_pose_manually_updated) - def update_instructions(self, has_set_object_poses_in_robot_frame: bool, robot_pose_confirmed: bool): + def update_instructions(self, has_set_object_poses_in_robot_frame: bool, robot_pose_confirmed: bool) -> None: self.has_confirmed_robot_pose = robot_pose_confirmed self.has_set_object_poses_in_robot_frame = has_set_object_poses_in_robot_frame and ( self.has_confirmed_robot_pose or not self.robot_configuration.has_no_robot() @@ -191,7 +191,7 @@ def update_instructions(self, has_set_object_poses_in_robot_frame: bool, robot_p "background-color: green;" if self.has_confirmed_robot_pose else "" ) - def on_pending_changes(self): + def on_pending_changes(self) -> None: if self.data_directory_has_data(): self.calibration_board_in_camera_frame_pose_widget.set_transformation_matrix( load_transformation_matrix(self.data_directory / "calibration_object_in_camera_frame_pose.yaml") @@ -209,17 +209,17 @@ def on_pending_changes(self): self.data_directory / "calibration_object_in_robot_base_frame_pose.yaml", ) - def on_tab_visibility_changed(self, is_current: bool): + def on_tab_visibility_changed(self, is_current: bool) -> None: pass - def hand_eye_configuration_update(self, hand_eye_configuration: HandEyeConfiguration): + def hand_eye_configuration_update(self, hand_eye_configuration: HandEyeConfiguration) -> None: self.hand_eye_configuration = hand_eye_configuration self.on_hand_eye_configuration_updated() - def marker_configuration_update(self, marker_configuration: MarkerConfiguration): + def marker_configuration_update(self, marker_configuration: MarkerConfiguration) -> None: self.marker_configuration = marker_configuration - def rotation_format_update(self, rotation_information: RotationInformation): + def rotation_format_update(self, rotation_information: RotationInformation) -> None: self.hand_eye_pose_widget.set_rotation_format(rotation_information) self.robot_pose_widget.set_rotation_format(rotation_information) self.calibration_board_in_camera_frame_pose_widget.set_rotation_format(rotation_information) @@ -227,7 +227,7 @@ def rotation_format_update(self, rotation_information: RotationInformation): self.markers_in_camera_frame_pose_widget.set_rotation_format(rotation_information) self.markers_in_robot_base_frame_pose_widget.set_rotation_format(rotation_information) - def robot_configuration_update(self, robot_configuration: RobotConfiguration): + def robot_configuration_update(self, robot_configuration: RobotConfiguration) -> None: self.robot_configuration = robot_configuration self.confirm_robot_pose_button.setVisible(self.robot_configuration.has_no_robot()) self.update_instructions( @@ -235,7 +235,7 @@ def robot_configuration_update(self, robot_configuration: RobotConfiguration): robot_pose_confirmed=self.has_confirmed_robot_pose, ) - def toggle_advanced_view(self, checked): + def toggle_advanced_view(self, checked: bool) -> None: if checked != self.advanced_view: self.top_grid_layout.removeWidget(self.robot_pose_widget) self.top_grid_layout.removeWidget(self.confirm_robot_pose_button) @@ -270,7 +270,7 @@ def toggle_advanced_view(self, checked): self.calibration_board_in_camera_frame_pose_widget.setVisible(show_calibration_board_pose) self.calibration_board_in_robot_base_frame_pose_widget.setVisible(show_calibration_board_pose) - def on_hand_eye_configuration_updated(self): + def on_hand_eye_configuration_updated(self) -> None: if self.hand_eye_configuration.calibration_object == CalibrationObject.Checkerboard: self.calibration_object_poses_layout.removeWidget(self.markers_in_camera_frame_pose_widget) self.calibration_object_poses_layout.removeWidget(self.markers_in_robot_base_frame_pose_widget) @@ -299,7 +299,7 @@ def on_hand_eye_configuration_updated(self): ) self.detection_visualization_widget.on_hand_eye_configuration_updated(self.hand_eye_configuration) - def confirm_robot_pose(self): + def confirm_robot_pose(self) -> None: self.update_instructions( has_set_object_poses_in_robot_frame=self.has_set_object_poses_in_robot_frame, robot_pose_confirmed=True ) @@ -307,10 +307,10 @@ def confirm_robot_pose(self): self.calculate_calibration_object_in_camera_frame_pose() self.update_projection.emit(True) - def on_confirm_robot_pose_button_clicked(self): + def on_confirm_robot_pose_button_clicked(self) -> None: self.confirm_robot_pose() - def on_robot_pose_manually_updated(self): + def on_robot_pose_manually_updated(self) -> None: self.update_instructions( has_set_object_poses_in_robot_frame=self.has_set_object_poses_in_robot_frame, robot_pose_confirmed=self.has_confirmed_robot_pose, @@ -319,13 +319,13 @@ def on_robot_pose_manually_updated(self): self.calculate_calibration_object_in_camera_frame_pose() self.update_projection.emit(True) - def on_actual_pose_updated(self, robot_target: RobotTarget): + def on_actual_pose_updated(self, robot_target: RobotTarget) -> None: self.robot_pose_widget.set_transformation_matrix(robot_target.pose) self.confirm_robot_pose() self.calculate_calibration_object_in_camera_frame_pose() self.update_projection.emit(True) - def on_target_pose_updated(self, robot_target: RobotTarget): + def on_target_pose_updated(self, robot_target: RobotTarget) -> None: self.robot_pose_widget.set_transformation_matrix(robot_target.pose) self.has_confirmed_robot_pose = False self.calculate_calibration_object_in_camera_frame_pose() @@ -336,7 +336,7 @@ def has_features_to_project(self) -> bool: self.has_confirmed_robot_pose or self.robot_configuration.can_control() ) and self.has_set_object_poses_in_robot_frame - def process_capture(self, frame: zivid.Frame, rgba: NDArray[Shape["N, M, 4"], UInt8], settings: SettingsPixelMappingIntrinsics): # type: ignore + def process_capture(self, frame: zivid.Frame, rgba: NDArray[Shape["N, M, 4"], UInt8], settings: SettingsPixelMappingIntrinsics) -> None: # type: ignore self.detected_markers = {} detection_result = ( zivid.calibration.detect_calibration_board(frame) @@ -378,7 +378,7 @@ def process_capture(self, frame: zivid.Frame, rgba: NDArray[Shape["N, M, 4"], UI self.calculate_calibration_object_in_robot_frame_pose() self.update_projection.emit(True) - def generate_projector_image(self, camera: zivid.Camera): + def generate_projector_image(self, camera: zivid.Camera) -> NDArray[Shape["N, M, 4"], UInt8]: # type: ignore projector_pixels = np.asarray( zivid.projection.pixels_from_3d_points(camera, self.detected_feature_points_in_camera_frame) ) @@ -412,7 +412,7 @@ def generate_projector_image(self, camera: zivid.Camera): ) return projector_image - def calculate_calibration_object_in_camera_frame_pose(self): + def calculate_calibration_object_in_camera_frame_pose(self) -> None: hand_eye_transform = self.hand_eye_pose_widget.transformation_matrix robot_transform = self.robot_pose_widget.transformation_matrix camera_frame_transform = ( @@ -433,7 +433,7 @@ def calculate_calibration_object_in_camera_frame_pose(self): } self.markers_in_camera_frame_pose_widget.set_markers(self.detected_marker_poses_in_camera_frame) - def calculate_calibration_object_in_robot_frame_pose(self): + def calculate_calibration_object_in_robot_frame_pose(self) -> None: hand_eye_transform = self.hand_eye_pose_widget.transformation_matrix robot_transform = self.robot_pose_widget.transformation_matrix robot_frame_transform = ( @@ -457,7 +457,7 @@ def calculate_calibration_object_in_robot_frame_pose(self): } self.markers_in_robot_base_frame_pose_widget.set_markers(self.detected_marker_poses_in_robot_frame) - def set_hand_eye_transformation_matrix(self, transformation_matrix: TransformationMatrix): + def set_hand_eye_transformation_matrix(self, transformation_matrix: TransformationMatrix) -> None: self.hand_eye_pose_widget.set_transformation_matrix(transformation_matrix) self.calculate_calibration_object_in_camera_frame_pose() self.update_projection.emit(True) diff --git a/modules/zividsamples/gui/verification/stitch_gui.py b/modules/zividsamples/gui/verification/stitch_gui.py index fa680476..b62681e4 100644 --- a/modules/zividsamples/gui/verification/stitch_gui.py +++ b/modules/zividsamples/gui/verification/stitch_gui.py @@ -12,8 +12,9 @@ from typing import Dict, List, Optional import zivid +from nptyping import NDArray, Shape, UInt8 from PyQt5.QtCore import pyqtSignal -from PyQt5.QtGui import QImage +from PyQt5.QtGui import QCloseEvent, QImage from PyQt5.QtWidgets import QCheckBox, QHBoxLayout, QPushButton, QVBoxLayout, QWidget from zividsamples.gui.robot.robot_control import RobotTarget from zividsamples.gui.verification.capture_at_pose_selection_widget import CaptureAtPose, CaptureAtPoseSelectionWidget @@ -23,6 +24,7 @@ from zividsamples.gui.wizard.hand_eye_configuration import HandEyeConfiguration from zividsamples.gui.wizard.robot_configuration import RobotConfiguration from zividsamples.gui.wizard.rotation_format_configuration import RotationInformation +from zividsamples.gui.wizard.settings_selector import SettingsPixelMappingIntrinsics from zividsamples.transformation_matrix import TransformationMatrix @@ -45,8 +47,8 @@ def __init__( robot_configuration: RobotConfiguration, hand_eye_configuration: HandEyeConfiguration, initial_rotation_information: RotationInformation, - parent=None, - ): + parent: Optional[QWidget] = None, + ) -> None: super().__init__(data_directory, parent) self.description = [ @@ -66,7 +68,7 @@ def __init__( self.connect_signals() self.update_instructions(captured=False, robot_pose_confirmed=False) - def create_widgets(self, initial_rotation_information: RotationInformation): + def create_widgets(self, initial_rotation_information: RotationInformation) -> None: self.robot_pose_widget = PoseWidget.Robot( eye_in_hand=self.hand_eye_configuration.eye_in_hand, display_mode=PoseWidgetDisplayMode.OnlyPose, @@ -87,7 +89,7 @@ def create_widgets(self, initial_rotation_information: RotationInformation): self.uniform_color_check_box.setChecked(True) self.point_cloud_widget = VisualizerWidget() - def setup_layout(self): + def setup_layout(self) -> None: layout = QVBoxLayout() left_panel = QVBoxLayout() right_panel = QVBoxLayout() @@ -108,7 +110,7 @@ def setup_layout(self): self.setLayout(layout) - def connect_signals(self): + def connect_signals(self) -> None: self.confirm_robot_pose_button.clicked.connect(self.on_confirm_robot_pose_button_clicked) self.capture_at_pose_selection_widget.capture_at_pose_clicked.connect(self.on_capture_at_pose_selected) self.capture_at_pose_selection_widget.selected_captures_updated.connect(self.update_stitched_view) @@ -116,7 +118,7 @@ def connect_signals(self): self.capture_at_pose_selection_widget.loading_finished.connect(self.loading_finished) self.uniform_color_check_box.stateChanged.connect(self.update_stitched_view) - def update_instructions(self, captured: bool, robot_pose_confirmed: bool): + def update_instructions(self, captured: bool, robot_pose_confirmed: bool) -> None: self.has_confirmed_robot_pose = robot_pose_confirmed self.instruction_steps = {} if self.robot_configuration.can_control(): @@ -132,7 +134,7 @@ def update_instructions(self, captured: bool, robot_pose_confirmed: bool): "background-color: green;" if self.has_confirmed_robot_pose else "" ) - def on_pending_changes(self): + def on_pending_changes(self) -> None: if self.data_directory_has_data(): self.capture_at_pose_selection_widget.on_clear_button_clicked() self.capture_at_pose_selection_widget.set_directory(self.data_directory) @@ -146,34 +148,34 @@ def on_pending_changes(self): def is_loading(self) -> bool: return self.capture_at_pose_selection_widget.is_loading() - def on_tab_visibility_changed(self, is_current: bool): + def on_tab_visibility_changed(self, is_current: bool) -> None: if is_current: self.update_stitched_view() else: self.point_cloud_widget.hide() - def hand_eye_configuration_update(self, hand_eye_configuration: HandEyeConfiguration): + def hand_eye_configuration_update(self, hand_eye_configuration: HandEyeConfiguration) -> None: self.hand_eye_configuration = hand_eye_configuration self.hand_eye_pose_widget.on_eye_in_hand_toggled(self.hand_eye_configuration.eye_in_hand) self.robot_pose_widget.on_eye_in_hand_toggled(self.hand_eye_configuration.eye_in_hand) - def rotation_format_update(self, rotation_information: RotationInformation): + def rotation_format_update(self, rotation_information: RotationInformation) -> None: self.hand_eye_pose_widget.set_rotation_format(rotation_information) self.robot_pose_widget.set_rotation_format(rotation_information) - def robot_configuration_update(self, robot_configuration: RobotConfiguration): + def robot_configuration_update(self, robot_configuration: RobotConfiguration) -> None: self.robot_configuration = robot_configuration self.confirm_robot_pose_button.setVisible(self.robot_configuration.has_no_robot()) self.update_instructions(captured=False, robot_pose_confirmed=self.has_confirmed_robot_pose) - def on_confirm_robot_pose_button_clicked(self): + def on_confirm_robot_pose_button_clicked(self) -> None: self.update_instructions(captured=False, robot_pose_confirmed=self.confirm_robot_pose_button.isChecked()) - def on_actual_pose_updated(self, robot_target: RobotTarget): + def on_actual_pose_updated(self, robot_target: RobotTarget) -> None: self.robot_pose_widget.set_transformation_matrix(robot_target.pose) self.update_instructions(captured=False, robot_pose_confirmed=True) - def update_stitched_view(self): + def update_stitched_view(self) -> None: capture_at_poses = self.capture_at_pose_selection_widget.get_selected_capture_at_poses() unorganized_point_cloud = zivid.UnorganizedPointCloud() for capture_at_pose in capture_at_poses: @@ -185,10 +187,10 @@ def update_stitched_view(self): unorganized_point_cloud = unorganized_point_cloud.voxel_downsampled(voxel_size=1, min_points_per_voxel=1) self.point_cloud_widget.set_point_cloud(unorganized_point_cloud) - def on_capture_at_pose_selected(self, capture_at_pose: CaptureAtPose): + def on_capture_at_pose_selected(self, capture_at_pose: CaptureAtPose) -> None: self.robot_pose_widget.set_transformation_matrix(capture_at_pose.robot_pose) - def process_capture(self, frame: zivid.Frame, _, __): # type: ignore + def process_capture(self, frame: zivid.Frame, _: NDArray[Shape["N, M, 4"], UInt8], __: SettingsPixelMappingIntrinsics) -> None: # type: ignore if self.has_confirmed_robot_pose: self.capture_at_pose_selection_widget.add_capture_at_pose( robot_pose=self.robot_pose_widget.get_transformation_matrix(), @@ -199,7 +201,7 @@ def process_capture(self, frame: zivid.Frame, _, __): # type: ignore self.update_stitched_view() self.update_instructions(captured=True, robot_pose_confirmed=False) - def set_hand_eye_transformation_matrix(self, transformation_matrix: TransformationMatrix): + def set_hand_eye_transformation_matrix(self, transformation_matrix: TransformationMatrix) -> None: self.hand_eye_pose_widget.set_transformation_matrix(transformation_matrix) def get_tab_widgets_in_order(self) -> List[QWidget]: @@ -209,6 +211,6 @@ def get_tab_widgets_in_order(self) -> List[QWidget]: widgets.extend(self.hand_eye_pose_widget.get_tab_widgets_in_order()) return widgets - def closeEvent(self, event) -> None: # pylint: disable=C0103 + def closeEvent(self, event: QCloseEvent) -> None: # pylint: disable=C0103 self.point_cloud_widget.close() super().closeEvent(event) diff --git a/modules/zividsamples/gui/verification/touch_gui.py b/modules/zividsamples/gui/verification/touch_gui.py index 55f4d220..74d9b7ed 100644 --- a/modules/zividsamples/gui/verification/touch_gui.py +++ b/modules/zividsamples/gui/verification/touch_gui.py @@ -11,13 +11,13 @@ """ from pathlib import Path -from typing import Dict, List +from typing import Dict, List, Optional import numpy as np import zivid from nptyping import NDArray, Shape, UInt8 from PyQt5.QtCore import pyqtSignal -from PyQt5.QtGui import QImage, QPixmap +from PyQt5.QtGui import QCloseEvent, QImage, QPixmap from PyQt5.QtWidgets import QHBoxLayout, QPushButton, QVBoxLayout, QWidget from zividsamples.gui.robot.robot_control import RobotTarget from zividsamples.gui.widgets.cv2_handler import CV2Handler @@ -46,8 +46,8 @@ def __init__( data_directory: Path, hand_eye_configuration: HandEyeConfiguration, initial_rotation_information: RotationInformation, - parent=None, - ): + parent: Optional[QWidget] = None, + ) -> None: super().__init__(data_directory, parent) self.cv2_handler = CV2Handler() @@ -67,7 +67,7 @@ def __init__( self.connect_signals() self.update_instructions(marker_confirmed=False, marker_captured=False) - def create_widgets(self, initial_rotation_information: RotationInformation): + def create_widgets(self, initial_rotation_information: RotationInformation) -> None: self.hand_eye_pose_widget = PoseWidget.HandEye( eye_in_hand=self.hand_eye_configuration.eye_in_hand, display_mode=PoseWidgetDisplayMode.OnlyPose, @@ -92,7 +92,7 @@ def create_widgets(self, initial_rotation_information: RotationInformation): self.confirm_marker_button.setCheckable(True) self.confirm_marker_button.setObjectName("Touch-confirm_marker_button") - def setup_layout(self): + def setup_layout(self) -> None: layout = QVBoxLayout() top_layout = QHBoxLayout() left_panel = QVBoxLayout() @@ -113,10 +113,10 @@ def setup_layout(self): self.setLayout(layout) - def connect_signals(self): + def connect_signals(self) -> None: self.confirm_marker_button.clicked.connect(self.on_confirm_marker_button_clicked) - def update_instructions(self, marker_confirmed: bool, marker_captured: bool): + def update_instructions(self, marker_confirmed: bool, marker_captured: bool) -> None: self.marker_confirmed = marker_confirmed self.instruction_steps = {} self.instruction_steps["Confirm marker to touch"] = self.marker_confirmed @@ -126,31 +126,31 @@ def update_instructions(self, marker_confirmed: bool, marker_captured: bool): self.confirm_marker_button.setChecked(self.marker_confirmed) self.confirm_marker_button.setStyleSheet("background-color: green;" if self.marker_confirmed else "") - def on_pending_changes(self): + def on_pending_changes(self) -> None: pass - def on_tab_visibility_changed(self, is_current): + def on_tab_visibility_changed(self, is_current: bool) -> None: pass - def on_confirm_marker_button_clicked(self): + def on_confirm_marker_button_clicked(self) -> None: self.update_instructions(marker_confirmed=self.confirm_marker_button.isChecked(), marker_captured=False) - def hand_eye_configuration_update(self, hand_eye_configuration: HandEyeConfiguration): + def hand_eye_configuration_update(self, hand_eye_configuration: HandEyeConfiguration) -> None: self.hand_eye_configuration = hand_eye_configuration self.hand_eye_pose_widget.on_eye_in_hand_toggled(self.hand_eye_configuration.eye_in_hand) self.robot_pose_widget.on_eye_in_hand_toggled(self.hand_eye_configuration.eye_in_hand) - def rotation_format_update(self, rotation_information: RotationInformation): + def rotation_format_update(self, rotation_information: RotationInformation) -> None: self.hand_eye_pose_widget.set_rotation_format(rotation_information) self.robot_pose_widget.set_rotation_format(rotation_information) - def robot_configuration_update(self, _: RobotConfiguration): + def robot_configuration_update(self, _: RobotConfiguration) -> None: pass - def on_actual_pose_updated(self, robot_target: RobotTarget): + def on_actual_pose_updated(self, robot_target: RobotTarget) -> None: self.robot_pose_widget.set_transformation_matrix(robot_target.pose) - def process_capture(self, frame: zivid.Frame, rgba: NDArray[Shape["N, M, 4"], UInt8], settings: SettingsPixelMappingIntrinsics): # type: ignore + def process_capture(self, frame: zivid.Frame, rgba: NDArray[Shape["N, M, 4"], UInt8], settings: SettingsPixelMappingIntrinsics) -> None: # type: ignore touch_configuration = self.touch_configuration_widget.touch_configuration detection_result = zivid.calibration.detect_markers( frame, @@ -197,9 +197,9 @@ def get_tab_widgets_in_order(self) -> List[QWidget]: widgets.append(self.confirm_marker_button) return widgets - def set_hand_eye_transformation_matrix(self, transformation_matrix: TransformationMatrix): + def set_hand_eye_transformation_matrix(self, transformation_matrix: TransformationMatrix) -> None: self.hand_eye_pose_widget.set_transformation_matrix(transformation_matrix) - def closeEvent(self, a0): + def closeEvent(self, a0: QCloseEvent) -> None: self.touch_configuration_widget.closeEvent(a0) return super().closeEvent(a0) diff --git a/modules/zividsamples/gui/verification/verification_buttons_widget.py b/modules/zividsamples/gui/verification/verification_buttons_widget.py index f4677886..a79cdbea 100644 --- a/modules/zividsamples/gui/verification/verification_buttons_widget.py +++ b/modules/zividsamples/gui/verification/verification_buttons_widget.py @@ -1,3 +1,5 @@ +from typing import Optional + from PyQt5.QtCore import pyqtSignal from PyQt5.QtWidgets import QGroupBox, QHBoxLayout, QPushButton, QWidget from zividsamples.gui.wizard.hand_eye_configuration import HandEyeConfiguration @@ -9,8 +11,8 @@ class HandEyeVerificationButtonsWidget(QWidget): def __init__( self, hand_eye_configuration: HandEyeConfiguration, - parent=None, - ): + parent: Optional[QWidget] = None, + ) -> None: super().__init__(parent) # Define buttons @@ -34,12 +36,12 @@ def __init__( self.setLayout(buttons_layout) - def on_project_button_clicked(self): + def on_project_button_clicked(self) -> None: self.project_button_clicked.emit() if self.project_button.isChecked(): self.project_button.setStyleSheet("background-color: green;") else: self.project_button.setStyleSheet("") - def on_hand_eye_configuration_updated(self, hand_eye_configuration: HandEyeConfiguration): + def on_hand_eye_configuration_updated(self, hand_eye_configuration: HandEyeConfiguration) -> None: self.project_button.setText(f"Project on {hand_eye_configuration.calibration_object.name}") diff --git a/modules/zividsamples/gui/widgets/aspect_ratio_label.py b/modules/zividsamples/gui/widgets/aspect_ratio_label.py index d9b95614..97ec8d70 100644 --- a/modules/zividsamples/gui/widgets/aspect_ratio_label.py +++ b/modules/zividsamples/gui/widgets/aspect_ratio_label.py @@ -1,37 +1,47 @@ +from typing import Optional + from PyQt5.QtCore import Qt -from PyQt5.QtWidgets import QDialog, QLabel, QVBoxLayout +from PyQt5.QtGui import QMouseEvent, QPixmap, QResizeEvent +from PyQt5.QtWidgets import QDialog, QGridLayout, QLabel, QVBoxLayout, QWidget class AspectRatioLabel(QLabel): - def __init__(self, title: str, pixmap, parent=None): + def __init__(self, title: str, pixmap: QPixmap, parent: Optional[QWidget] = None): super().__init__(parent) self.setMinimumHeight(200) self.original_pixmap = pixmap self.title = title self.setPixmap() - def set_original_pixmap(self, pixmap): + def set_original_pixmap(self, pixmap: QPixmap) -> None: self.original_pixmap = pixmap self.setPixmap() - def setPixmap(self): + def setPixmap(self) -> None: super().setPixmap(self.scaledPixmap()) - def resizeEvent(self, _): + def resizeEvent(self, _: QResizeEvent) -> None: if self.original_pixmap: self.setPixmap() - def scaledPixmap(self): + def scaledPixmap(self) -> QPixmap: size = self.size() return self.original_pixmap.scaledToHeight(size.height(), Qt.SmoothTransformation) - def setFixedHeight(self, height): + def setFixedHeight(self, height: int) -> None: constrained_height = min(max(self.minimumHeight(), height), self.maximumHeight()) super().setFixedHeight(constrained_height) # pylint: disable=too-many-positional-arguments - def setHeightFromGrid(self, grid_layout, row_start, row_span, col_start, col_span): + def setHeightFromGrid( + self, + grid_layout: QGridLayout, + row_start: int, + row_span: int, + col_start: int, + col_span: int, + ) -> None: max_height = 0 if row_start > 0 else grid_layout.verticalSpacing() for row in range(row_start, row_start + row_span): max_height += grid_layout.verticalSpacing() @@ -48,11 +58,11 @@ def setHeightFromGrid(self, grid_layout, row_start, row_span, col_start, col_spa self.setFixedHeight(max_height) - def mousePressEvent(self, event): + def mousePressEvent(self, event: QMouseEvent) -> None: if event.button() == Qt.LeftButton: self.show_original_pixmap() - def show_original_pixmap(self): + def show_original_pixmap(self) -> None: dialog = QDialog(self) dialog.setWindowTitle(self.title) dialog.setModal(True) diff --git a/modules/zividsamples/gui/widgets/camera_buttons_widget.py b/modules/zividsamples/gui/widgets/camera_buttons_widget.py index 20151f3f..242999a7 100644 --- a/modules/zividsamples/gui/widgets/camera_buttons_widget.py +++ b/modules/zividsamples/gui/widgets/camera_buttons_widget.py @@ -11,9 +11,9 @@ class CameraButtonsWidget(QWidget): def __init__( self, - capture_button_text="Capture", - parent=None, - ): + capture_button_text: str = "Capture", + parent: Optional[QWidget] = None, + ) -> None: super().__init__(parent) # Define buttons @@ -49,7 +49,7 @@ def __init__( self.setLayout(layout) - def on_capture_button_clicked(self): + def on_capture_button_clicked(self) -> None: self.capture_button.setChecked(True) self.capture_button.setStyleSheet("background-color: yellow;") QApplication.processEvents() @@ -57,13 +57,13 @@ def on_capture_button_clicked(self): self.capture_button.setChecked(False) self.capture_button.setStyleSheet("") - def on_connect_button_clicked(self): + def on_connect_button_clicked(self) -> None: self.connect_button.setChecked(True) self.connect_button.setStyleSheet("background-color: yellow;") QApplication.processEvents() self.connect_button_clicked.emit() - def set_connection_status(self, camera: Optional[zivid.Camera]): + def set_connection_status(self, camera: Optional[zivid.Camera]) -> None: if camera is None: self.connect_button.setText("Connect") self.connect_button.setChecked(False) @@ -78,19 +78,19 @@ def set_connection_status(self, camera: Optional[zivid.Camera]): self.connect_button.setStyleSheet("background-color: green;" if camera.state.connected else "") self.capture_button.setEnabled(camera.state.connected) - def set_information(self, text: str): + def set_information(self, text: str) -> None: if text == "": self.information_label.hide() else: self.information_label.show() self.information_label.setText(text) - def disable_buttons(self, capture_tooltip: str = ""): + def disable_buttons(self, capture_tooltip: str = "") -> None: self.connect_button.setEnabled(False) self.capture_button.setEnabled(False) self.capture_button.setToolTip(capture_tooltip) - def enable_buttons(self): + def enable_buttons(self) -> None: self.connect_button.setEnabled(True) self.capture_button.setEnabled(True) self.capture_button.setToolTip("") diff --git a/modules/zividsamples/gui/widgets/cv2_handler.py b/modules/zividsamples/gui/widgets/cv2_handler.py index 0d8abed1..7c5a377d 100644 --- a/modules/zividsamples/gui/widgets/cv2_handler.py +++ b/modules/zividsamples/gui/widgets/cv2_handler.py @@ -48,13 +48,13 @@ def __init__(self): self.cv2 = cv2 def project_points(self, points: NDArray[Shape["N, 3"], np.float32], intrinsics: zivid.CameraIntrinsics) -> NDArray[Shape["N, 2"], np.float32]: # type: ignore - points_2d, _ = self.cv2.projectPoints( + points_2d = self.cv2.projectPoints( np.array(points, dtype=np.float32), rvec=np.zeros(3), tvec=np.zeros(3), cameraMatrix=_zivid_camera_matrix_to_opencv_camera_matrix(intrinsics.camera_matrix), distCoeffs=_zivid_distortion_coefficients_to_opencv_distortion_coefficients(intrinsics.distortion), - ) + )[0] return np.array(points_2d).reshape(-1, 2) def points_of_interest_in_2d_from_3d( @@ -90,7 +90,7 @@ def draw_detected_markers( detected_markers: List[MarkerShape], rgb: NDArray[Shape["N, M, 3"], UInt8], # type: ignore pixel_mapping: PixelMapping, - ): + ) -> NDArray[Shape["N, M, 3"], UInt8]: # type: ignore detected_corners = [ np.array(marker.corners_in_pixel_coordinates).reshape((4, 1, 2)) for marker in detected_markers ] @@ -153,16 +153,16 @@ def draw_3d_points_in_2d( rgb: NDArray[Shape["N, M, 3"], UInt8], # type: ignore points: NDArray[Shape["N, 3"], np.int32], # type: ignore circle_color: Tuple[int, int, int, int] = (0, 255, 0, 255), - ): + ) -> NDArray[Shape["N, M, 3"], UInt8]: # type: ignore circle_size_in_pixels: int = 8 np_points = np.array(points, dtype=np.float32) - projected_points, _ = self.cv2.projectPoints( + projected_points = self.cv2.projectPoints( np_points, rvec=np.zeros(3), tvec=np.zeros(3), cameraMatrix=_zivid_camera_matrix_to_opencv_camera_matrix(intrinsics.camera_matrix), distCoeffs=_zivid_distortion_coefficients_to_opencv_distortion_coefficients(intrinsics.distortion), - ) + )[0] projected_points = projected_points.reshape(-1, 2).astype(int) for original_point, projected_point in zip(np_points, projected_points, strict=False): self.cv2.circle( @@ -174,7 +174,7 @@ def draw_3d_points_in_2d( lineType=self.cv2.LINE_AA, ) text = f"[{original_point[0]:6.1f}, {original_point[1]:6.1f}, {original_point[2]:6.1f}]" - text_size, _ = self.cv2.getTextSize(text, self.cv2.FONT_HERSHEY_SIMPLEX, 0.5, 1) + text_size = self.cv2.getTextSize(text, self.cv2.FONT_HERSHEY_SIMPLEX, 0.5, 1)[0] text_x = projected_point[0] - text_size[0] // 2 if projected_point[1] < rgb.shape[0] // 3: text_y = projected_point[1] + circle_size_in_pixels + text_size[1] * 2 @@ -204,7 +204,7 @@ def draw_circles( image: NDArray[Shape["N, M, 3"], UInt8], # type: ignore points: NDArray[Shape["N, 2"], np.int32], # type: ignore circle_color: Tuple[int, int, int, int] = (0, 255, 0, 255), - ): + ) -> None: circle_size_in_pixels = 2 for point in points: self.cv2.circle( @@ -237,10 +237,10 @@ def draw_polygons( lineType=self.cv2.LINE_AA, ) - def draw_fov_division_from_3d(self, rgb: NDArray[Shape["N, M, 3"], UInt8], points_of_interest_3d: PointsOfInterest, intrinsics: zivid.CameraIntrinsics): # type: ignore + def draw_fov_division_from_3d(self, rgb: NDArray[Shape["N, M, 3"], UInt8], points_of_interest_3d: PointsOfInterest, intrinsics: zivid.CameraIntrinsics) -> None: # type: ignore self.draw_fov_division(rgb, self.points_of_interest_in_2d_from_3d(points_of_interest_3d, intrinsics)) - def draw_fov_division(self, rgb: NDArray[Shape["N, M, 3"], UInt8], points_of_interest_2d: PointsOfInterest2D, use_bgr: bool = False): # type: ignore + def draw_fov_division(self, rgb: NDArray[Shape["N, M, 3"], UInt8], points_of_interest_2d: PointsOfInterest2D, use_bgr: bool = False) -> None: # type: ignore thickness = 8 lines_2d = points_of_interest_2d.lines_2d() self.draw_polygons( @@ -268,5 +268,5 @@ def draw_fov_division(self, rgb: NDArray[Shape["N, M, 3"], UInt8], points_of_int rgb, points=points_of_interest_2d.center_corners, color=(0, 255, 0, 255), thickness=thickness ) - def apply_colormap(self, depth_map_uint8: NDArray[Shape["N, M, 3"], UInt8]): # type: ignore + def apply_colormap(self, depth_map_uint8: NDArray[Shape["N, M, 3"], UInt8]) -> NDArray[Shape["N, M, 3"], UInt8]: # type: ignore return self.cv2.applyColorMap(depth_map_uint8, self.cv2.COLORMAP_VIRIDIS) diff --git a/modules/zividsamples/gui/widgets/detection_visualization.py b/modules/zividsamples/gui/widgets/detection_visualization.py index 94c7ce7d..8860d138 100644 --- a/modules/zividsamples/gui/widgets/detection_visualization.py +++ b/modules/zividsamples/gui/widgets/detection_visualization.py @@ -18,7 +18,12 @@ class DetectionVisualizationWidget(QWidget): calibration_object_pixmap: Dict[CalibrationObject, Optional[QPixmap]] = {} reset_zoom_on_next_calibration_object_image_update: bool = True - def __init__(self, hand_eye_configuration: HandEyeConfiguration, hide_descriptive_image: bool = False, parent=None): + def __init__( + self, + hand_eye_configuration: HandEyeConfiguration, + hide_descriptive_image: bool = False, + parent: Optional[QWidget] = None, + ): super().__init__(parent) self.calibration_object_pixmap = { @@ -70,7 +75,7 @@ def __init__(self, hand_eye_configuration: HandEyeConfiguration, hide_descriptiv if hide_descriptive_image: self.descriptive_image_label.hide() - def update_layout(self): + def update_layout(self) -> None: self.group_box.setTitle(f"{self.hand_eye_configuration.calibration_object.name} in Camera Frame") self.update_calibration_object_image() descriptive_image = ( @@ -83,7 +88,7 @@ def update_layout(self): int(self.descriptive_image_width * descriptive_image.height() / descriptive_image.width()) ) - def on_hand_eye_configuration_updated(self, hand_eye_configuration: HandEyeConfiguration): + def on_hand_eye_configuration_updated(self, hand_eye_configuration: HandEyeConfiguration) -> None: last_calibration_object = self.hand_eye_configuration.calibration_object self.hand_eye_configuration = copy.deepcopy(hand_eye_configuration) self.reset_zoom_on_next_calibration_object_image_update = ( @@ -91,7 +96,7 @@ def on_hand_eye_configuration_updated(self, hand_eye_configuration: HandEyeConfi ) self.update_layout() - def update_calibration_object_image(self, reset_zoom: bool = False): + def update_calibration_object_image(self, reset_zoom: bool = False) -> None: calibration_object_pixmap = self.calibration_object_pixmap[self.hand_eye_configuration.calibration_object] self.error_message_label.setVisible(calibration_object_pixmap is None) self.calibration_object_image_viewer.setVisible(calibration_object_pixmap is not None) @@ -115,7 +120,7 @@ def set_image(self, qimage_rgba: QImage, reset_zoom: bool = False) -> None: def set_rgba_image(self, rgba: NDArray[Shape["N, M, 4"], UInt8], reset_zoom: bool = False) -> None: # type: ignore self.set_image(QImage(rgba.data, rgba.shape[1], rgba.shape[0], QImage.Format_RGBA8888), reset_zoom) - def set_error_message(self, error_message: str): + def set_error_message(self, error_message: str) -> None: self.error_message_label.setText(error_message) self.reset_zoom_on_next_calibration_object_image_update = True self.error_message_label.show() diff --git a/modules/zividsamples/gui/widgets/fov.py b/modules/zividsamples/gui/widgets/fov.py index f2bde5bd..d6a3ca52 100644 --- a/modules/zividsamples/gui/widgets/fov.py +++ b/modules/zividsamples/gui/widgets/fov.py @@ -25,7 +25,7 @@ class CameraFOV: height: float @classmethod - def from_model_and_distance(cls, camera_info: zivid.CameraInfo, distance: float) -> "CameraFOV": + def from_model_and_distance(cls: type["CameraFOV"], camera_info: zivid.CameraInfo, distance: float) -> "CameraFOV": fov_constants = CameraFOVConstants(camera_info) z = distance @@ -52,7 +52,9 @@ def __init__(self, camera_info: zivid.CameraInfo): self.width_at_distances: List[Dict[str, float]] = self.camera_parameters.loc[camera_info.model_name]["key_b"]["width_at_distances"] # type: ignore self.height_at_distances: List[Dict[str, float]] = self.camera_parameters.loc[camera_info.model_name]["key_b"]["height_at_distances"] # type: ignore - def _find_value_range(self, distance, value_at_distances): + def _find_value_range( + self, distance: float, value_at_distances: List[Dict[str, float]] + ) -> Dict[str, Dict[str, float]]: if distance < value_at_distances[0]["distance"]: raise RuntimeError(f"No data available to interpolate at this distance {distance}") for value_at_distance_min, value_at_distance_max in zip( @@ -80,19 +82,19 @@ def _find_value_range(self, distance, value_at_distances): }, } - def _find_width_range(self, distance): + def _find_width_range(self, distance: float) -> Dict[str, Dict[str, float]]: return self._find_value_range( distance, self.width_at_distances, ) - def _find_height_range(self, distance): + def _find_height_range(self, distance: float) -> Dict[str, Dict[str, float]]: return self._find_value_range( distance, self.height_at_distances, ) - def _value_at_distance(self, distance, value_range): + def _value_at_distance(self, distance: float, value_range: Dict[str, Dict[str, float]]) -> float: return ( (value_range["value"]["max"] - value_range["value"]["min"]) / (value_range["distance"]["max"] - value_range["distance"]["min"]) @@ -217,13 +219,13 @@ def point_in_polygon(self, point: Tuple[float, float], polygon: np.ndarray) -> b @classmethod def from_points_of_interest_and_camera( - cls, camera_info: zivid.CameraInfo, point_xyz: NDArray[Shape["3"], Float32], points_of_interest: PointsOfInterest # type: ignore + cls: type["PositionInFOV"], camera_info: zivid.CameraInfo, point_xyz: NDArray[Shape["3"], Float32], points_of_interest: PointsOfInterest # type: ignore ) -> "PositionInFOV": - def to_2d(points3d): + def to_2d(points3d: np.ndarray) -> np.ndarray: return points3d[:, :2] - def regions(points_of_interest: PointsOfInterest): + def regions(points_of_interest: PointsOfInterest) -> Dict[LocationInFOV, np.ndarray]: return { # Recall, for each line, the first point is the one on the center rectangle LocationInFOV.Center: to_2d(points_of_interest.center_corners), @@ -338,7 +340,7 @@ def as_dict(self) -> dict: } @classmethod - def from_dict(cls, data: dict) -> "PositionInFOV": + def from_dict(cls: type["PositionInFOV"], data: dict) -> "PositionInFOV": return cls( distance=DistanceInFOV[data["distance"]], location=LocationInFOV[data["location"]], diff --git a/modules/zividsamples/gui/widgets/image_viewer.py b/modules/zividsamples/gui/widgets/image_viewer.py index 774a5f78..7d160854 100644 --- a/modules/zividsamples/gui/widgets/image_viewer.py +++ b/modules/zividsamples/gui/widgets/image_viewer.py @@ -1,10 +1,12 @@ -from PyQt5.QtCore import QRectF, Qt, pyqtSlot -from PyQt5.QtGui import QImage, QPainter, QPixmap -from PyQt5.QtWidgets import QDialog, QGraphicsPixmapItem, QGraphicsScene, QGraphicsView, QVBoxLayout +from typing import Optional + +from PyQt5.QtCore import QRectF, QSize, Qt, pyqtSlot +from PyQt5.QtGui import QImage, QPainter, QPixmap, QWheelEvent +from PyQt5.QtWidgets import QDialog, QGraphicsPixmapItem, QGraphicsScene, QGraphicsView, QVBoxLayout, QWidget class ImageViewer(QGraphicsView): - def __init__(self, parent=None): + def __init__(self, parent: Optional[QWidget] = None) -> None: super().__init__(parent) self.is_first = True self.setScene(QGraphicsScene(self)) @@ -15,7 +17,7 @@ def __init__(self, parent=None): self._zoom = 0 @pyqtSlot(QPixmap, bool) - def set_pixmap(self, image: QPixmap, reset_zoom: bool = False): + def set_pixmap(self, image: QPixmap, reset_zoom: bool = False) -> None: self.scene().clear() pixmap_item = QGraphicsPixmapItem(image) self.scene().addItem(pixmap_item) @@ -25,7 +27,7 @@ def set_pixmap(self, image: QPixmap, reset_zoom: bool = False): self._zoom = 0 self.fitInView(self.sceneRect(), Qt.KeepAspectRatio) - def wheelEvent(self, event): + def wheelEvent(self, event: QWheelEvent) -> None: if event.angleDelta().y() > 0: factor = 1.25 self._zoom += 1 @@ -40,7 +42,7 @@ def wheelEvent(self, event): else: self._zoom = 0 - def resize(self, event): + def resize(self, event: QSize) -> None: super().resize(event) print("Resizing image_viewer") self._zoom = 0 @@ -48,7 +50,7 @@ def resize(self, event): class ImageViewerDialog(QDialog): - def __init__(self, qimage: QImage, title: str = "Image Viewer", parent=None): + def __init__(self, qimage: QImage, title: str = "Image Viewer", parent: Optional[QWidget] = None) -> None: super().__init__(parent) self.image_viewer = ImageViewer() self.image_viewer.set_pixmap(QPixmap.fromImage(qimage), reset_zoom=True) diff --git a/modules/zividsamples/gui/widgets/live_2d_widget.py b/modules/zividsamples/gui/widgets/live_2d_widget.py index b357ccc5..5c9dac15 100644 --- a/modules/zividsamples/gui/widgets/live_2d_widget.py +++ b/modules/zividsamples/gui/widgets/live_2d_widget.py @@ -24,7 +24,7 @@ def __init__( capture_function: Optional[Callable[[zivid.Settings2D], zivid.Frame2D]] = None, settings_2d: Optional[zivid.Settings2D] = None, camera: Optional[zivid.Camera] = None, - parent=None, + parent: Optional[QWidget] = None, ): super().__init__(parent) @@ -47,11 +47,13 @@ def __init__( layout.addWidget(self.group_box) self.setLayout(layout) - def setMinimumHeight(self, height, aspect_ratio: float = 4 / 3): + def setMinimumHeight(self, height: int, aspect_ratio: float = 4 / 3) -> None: self.live_2d_image.setMinimumHeight(height) self.live_2d_image.setMinimumWidth(int(height * aspect_ratio)) - def rgb_to_grayscale(self, rgba_image): + def rgb_to_grayscale( + self, rgba_image: NDArray[Shape["H, W, 4"], UInt8] # type: ignore + ) -> NDArray[Shape["H, W"], np.float64]: # type: ignore return np.dot(rgba_image[..., :3], [0.299, 0.587, 0.114]) def update_exposure_based_on_relative_brightness(self, settings_2d: zivid.Settings2D) -> zivid.Settings2D: @@ -85,7 +87,7 @@ def update_exposure_based_on_relative_brightness(self, settings_2d: zivid.Settin acquisition.brightness = 0.1 return settings_2d - def update_settings_2d(self, settings_2d: zivid.Settings2D, camera_model: str): + def update_settings_2d(self, settings_2d: zivid.Settings2D, camera_model: str) -> None: settings_2d_copy = zivid.Settings2D.from_serialized(settings_2d.serialize()) if camera_model in [ zivid.CameraInfo.Model.zivid2PlusMR60, @@ -109,7 +111,7 @@ def update_settings_2d(self, settings_2d: zivid.Settings2D, camera_model: str): self.settings_2d.acquisitions[0].aperture = 4 self.settings_2d.acquisitions[0].gain = 2.0 - def start_live_2d(self): + def start_live_2d(self) -> None: if self.live_thread: if self.live_thread.is_alive(): return @@ -131,7 +133,7 @@ def stop_live_2d(self) -> bool: self.current_rgba = None return False - def live_2d(self): + def live_2d(self) -> None: self.live_active = True while self.live_active: if not self.capture(): @@ -141,7 +143,7 @@ def live_2d(self): def is_active(self) -> bool: return self.live_active - def show_error_message(self, error_message: str): + def show_error_message(self, error_message: str) -> None: error_pixmap = QPixmap(self.live_2d_image.size()) error_pixmap.fill(Qt.gray) # Set background color painter = QPainter(error_pixmap) diff --git a/modules/zividsamples/gui/widgets/pointcloud_visualizer.py b/modules/zividsamples/gui/widgets/pointcloud_visualizer.py index dd3bb691..b0ac1f70 100644 --- a/modules/zividsamples/gui/widgets/pointcloud_visualizer.py +++ b/modules/zividsamples/gui/widgets/pointcloud_visualizer.py @@ -9,11 +9,11 @@ class VisualizerWidget: visualizer_thread: threading.Thread visualizer: zivid.visualization.Visualizer - def __init__(self): + def __init__(self) -> None: self.visualizer_thread = threading.Thread(target=self.run, daemon=True) self.visualizer_thread.start() - def run(self): + def run(self) -> None: self.visualizer = zivid.visualization.Visualizer() self.visualizer.set_window_title("Zivid Point Cloud Visualizer") self.visualizer.colors_enabled = True @@ -22,14 +22,14 @@ def run(self): self.visualizer.run() self.visualizer.release() - def set_point_cloud(self, data: Union[zivid.Frame, zivid.PointCloud, zivid.UnorganizedPointCloud]): + def set_point_cloud(self, data: Union[zivid.Frame, zivid.PointCloud, zivid.UnorganizedPointCloud]) -> None: if not self.visualizer_thread.is_alive(): self.visualizer_thread = threading.Thread(target=self.run, daemon=True) self.visualizer_thread.start() sleep(0.2) # Give some time for the thread to start self.visualizer.show(data) - def hide(self): + def hide(self) -> None: if self.visualizer_thread.is_alive(): self.visualizer.hide() diff --git a/modules/zividsamples/gui/widgets/pose_widget.py b/modules/zividsamples/gui/widgets/pose_widget.py index 46b20288..01c75d59 100644 --- a/modules/zividsamples/gui/widgets/pose_widget.py +++ b/modules/zividsamples/gui/widgets/pose_widget.py @@ -2,7 +2,7 @@ from abc import abstractmethod from enum import Enum from pathlib import Path -from typing import Dict, List, Optional, Tuple +from typing import Dict, List, Optional, Sequence, Tuple import numpy as np import zivid @@ -44,7 +44,7 @@ def __init__( initial_transformation_matrix: TransformationMatrix = TransformationMatrix(), descriptive_image_paths: Optional[Tuple[Path, Path]] = None, read_only: bool = False, - parent=None, + parent: Optional[QWidget] = None, ): super().__init__(parent) self.title = title @@ -75,7 +75,7 @@ def __init__( self.setup_base_layout() self.setup_base_connections() - def setup_base_widgets(self, title, descriptive_image_paths: Optional[Tuple[Path, Path]]): + def setup_base_widgets(self, title: str, descriptive_image_paths: Optional[Tuple[Path, Path]]) -> None: if self.display_mode != PoseWidgetDisplayMode.OnlyPose: self.rotation_format_selection_widget = RotationFormatSelectionWidget(self.rotation_information) @@ -87,38 +87,38 @@ def setup_base_widgets(self, title, descriptive_image_paths: Optional[Tuple[Path else: self.descriptive_image_label = None - def setup_base_layout(self): + def setup_base_layout(self) -> None: if self.display_mode == PoseWidgetDisplayMode.OnlyPose: self.grid_layout = QGridLayout() else: self.grid_layout = self.rotation_format_selection_widget.grid_layout - def setup_base_connections(self): + def setup_base_connections(self) -> None: if self.display_mode != PoseWidgetDisplayMode.OnlyPose: self.rotation_format_selection_widget.rotation_format_update.connect(self.on_rotation_format_update) def get_rotation_format(self) -> RotationInformation: return self.rotation_information - def set_rotation_format(self, rotation_format: RotationInformation): + def set_rotation_format(self, rotation_format: RotationInformation) -> None: self.rotation_information = rotation_format if self.display_mode != PoseWidgetDisplayMode.OnlyPose: self.rotation_format_selection_widget.set_rotation_format(rotation_format) self.update_from_transformation_matrix() - def on_eye_in_hand_toggled(self, eye_in_hand: bool): + def on_eye_in_hand_toggled(self, eye_in_hand: bool) -> None: descriptive_image = self.descriptive_image_eye_in_hand if eye_in_hand else self.descriptive_image_eye_to_hand if descriptive_image is not None and self.descriptive_image_label is not None: self.descriptive_image_label.set_original_pixmap(descriptive_image) - def get_transformation_matrix(self): + def get_transformation_matrix(self) -> TransformationMatrix: return self.transformation_matrix - def set_transformation_matrix(self, transformation_matrix: TransformationMatrix): + def set_transformation_matrix(self, transformation_matrix: TransformationMatrix) -> None: self.transformation_matrix = transformation_matrix self.update_from_transformation_matrix() - def on_rotation_format_update(self, rotation_information: RotationInformation): + def on_rotation_format_update(self, rotation_information: RotationInformation) -> None: self.rotation_information = rotation_information self.update_from_transformation_matrix() @@ -126,7 +126,7 @@ def on_rotation_format_update(self, rotation_information: RotationInformation): def update_from_transformation_matrix(self) -> None: raise RuntimeError("Method not implemented") - def rotation_from_parameters(self, rotation_parameters) -> Rotation: + def rotation_from_parameters(self, rotation_parameters: Sequence[float]) -> Rotation: rotation = None if self.rotation_information.rotation_format.name == "Angle-Axis": rotvec = np.asarray(rotation_parameters[1:]) * rotation_parameters[0] @@ -175,10 +175,10 @@ def parameters_from_rotation(self, rotation: Rotation) -> List[float]: ) return parameters - def zivid_transformation_matrix(self): + def zivid_transformation_matrix(self) -> zivid.Matrix4x4: return zivid.Matrix4x4(self.transformation_matrix.as_matrix()) - def _rotation_label_text(self): + def _rotation_label_text(self) -> str: degrees = ( " (°)" if self.rotation_information.use_degrees @@ -211,7 +211,7 @@ def __init__( initial_transformation_matrix: TransformationMatrix = TransformationMatrix(), descriptive_image_paths: Optional[Tuple[Path, Path]] = None, read_only: bool = False, - parent=None, + parent: Optional[QWidget] = None, ): super().__init__( title, @@ -232,7 +232,7 @@ def __init__( self.update_from_transformation_matrix() self.toggle_advanced_section(display_mode == PoseWidgetDisplayMode.Advanced) - def setup_widgets(self): + def setup_widgets(self) -> None: self.translation_parameters_label = QLabel() self.translation_parameters_label.setText("Translation") self.translation_parameter_editors: List[QLineEdit] = [QLineEdit() for _ in range(3)] @@ -254,7 +254,7 @@ def setup_widgets(self): self.pose_text = QLineEdit() self.pose_text.setReadOnly(self.read_only) - def setup_layout(self): + def setup_layout(self) -> None: self.group_box = QGroupBox(self.title) @@ -319,14 +319,14 @@ def setup_layout(self): main_layout.addWidget(self.group_box) self.setLayout(main_layout) - def setup_connections(self): + def setup_connections(self) -> None: for parameter_editor in self.translation_parameter_editors: parameter_editor.editingFinished.connect(self.on_translation_parameter_changed) for parameter_editor in self.rotation_parameter_editors: parameter_editor.editingFinished.connect(self.on_rotation_parameter_changed) self.pose_text.editingFinished.connect(self.on_pose_text_changed) - def toggle_advanced_section(self, checked: bool): + def toggle_advanced_section(self, checked: bool) -> None: for widget in self.advanced_widgets: widget.setVisible(checked) if self.descriptive_image_label is not None: @@ -375,7 +375,7 @@ def update_from_transformation_matrix(self) -> None: self.reset_parameter_editor_styles() self.pose_updated.emit() - def on_rotation_parameter_changed(self): + def on_rotation_parameter_changed(self) -> None: modified_index = int(self.sender().objectName().split("_")[-1]) try: update_parameter = parameter_text_to_float(self.sender().text()) @@ -410,7 +410,7 @@ def on_rotation_parameter_changed(self): self.transformation_matrix.rotation = updated_rotation self.update_from_transformation_matrix() - def on_translation_parameter_changed(self): + def on_translation_parameter_changed(self) -> None: modified_index = int(self.sender().objectName().split("_")[-1]) try: update_parameter = parameter_text_to_float(self.sender().text()) @@ -425,7 +425,7 @@ def on_translation_parameter_changed(self): self.transformation_matrix.translation[i] = parameter_text_to_float(parameter_editor.text()) self.update_from_transformation_matrix() - def on_pose_text_changed(self): + def on_pose_text_changed(self) -> None: parameter_list = re.split(r"[,\s]+", self.pose_text.text().strip()) if len(parameter_list) < (3 + len(self.rotation_parameters)): # Wait for user to finish entering data @@ -439,7 +439,7 @@ def on_pose_text_changed(self): self.transformation_matrix.rotation = updated_rotation self.update_from_transformation_matrix() - def reset_parameter_editor_styles(self): + def reset_parameter_editor_styles(self) -> None: for parameter_editor in self.rotation_parameter_editors: parameter_editor.setStyleSheet("") @@ -457,12 +457,12 @@ def get_tab_widgets_in_order(self) -> List[QWidget]: @classmethod def HandEye( - cls, + cls: type["PoseWidget"], eye_in_hand: bool, display_mode: PoseWidgetDisplayMode = PoseWidgetDisplayMode.Basic, initial_rotation_information: RotationInformation = RotationInformation(), initial_transformation_matrix: TransformationMatrix = TransformationMatrix(), - ): + ) -> "PoseWidget": ee_camera_pose_img_path = get_image_file_path( "hand-eye-robot-and-calibration-board-camera-on-robot-ee-camera-pose-low-res.png" ) @@ -480,12 +480,12 @@ def HandEye( @classmethod def Robot( - cls, + cls: type["PoseWidget"], eye_in_hand: bool, display_mode: PoseWidgetDisplayMode = PoseWidgetDisplayMode.Basic, initial_rotation_information: RotationInformation = RotationInformation(), initial_transformation_matrix: TransformationMatrix = TransformationMatrix(), - ): + ) -> "PoseWidget": robot_ee_pose_img_path = get_image_file_path( "hand-eye-robot-and-calibration-board-camera-on-robot-robot-ee-pose-low-res.png" ) @@ -501,12 +501,12 @@ def Robot( @classmethod def CalibrationBoardInCameraFrame( - cls, + cls: type["PoseWidget"], eye_in_hand: bool, display_mode: PoseWidgetDisplayMode = PoseWidgetDisplayMode.Basic, initial_rotation_information: RotationInformation = RotationInformation(), calibration_object_in_camera_frame_pose: TransformationMatrix = TransformationMatrix(), - ): + ) -> "PoseWidget": eih_camera_object_pose_img_path = get_image_file_path( "hand-eye-robot-and-calibration-board-camera-on-robot-camera-object-pose-low-res.png" ) @@ -525,12 +525,12 @@ def CalibrationBoardInCameraFrame( @classmethod def CalibrationBoardInRobotFrame( - cls, + cls: type["PoseWidget"], eye_in_hand: bool, display_mode: PoseWidgetDisplayMode = PoseWidgetDisplayMode.Basic, initial_rotation_information: RotationInformation = RotationInformation(), calibration_object_in_robot_base_frame_pose: TransformationMatrix = TransformationMatrix(), - ): + ) -> "PoseWidget": eih_robot_object_pose_img_path = get_image_file_path( "hand-eye-robot-and-calibration-board-camera-on-robot-robot-object-pose-low-res.png" ) @@ -564,7 +564,7 @@ def __init__( display_mode: PoseWidgetDisplayMode, descriptive_image_paths: Optional[Tuple[Path, Path]] = None, read_only: bool = False, - parent=None, + parent: Optional[QWidget] = None, ): super().__init__( title=title, @@ -579,14 +579,14 @@ def __init__( self.setup_widgets() self.setup_layout() - def setup_widgets(self): + def setup_widgets(self) -> None: self.ids_title_label = QLabel("IDs") self.translation_title_label = QLabel("Translation") self.rotation_title_label = QLabel(self._rotation_label_text()) self.translation_parameter_labels = {} self.rotation_parameter_labels = {} - def setup_layout(self): + def setup_layout(self) -> None: self.group_box = QGroupBox(self.title) self.grid_layout = QGridLayout() @@ -627,7 +627,7 @@ def setup_layout(self): self.update_layout() - def update_layout(self): + def update_layout(self) -> None: row_start = row_offset = 1 if self.display_mode == PoseWidgetDisplayMode.OnlyPose else 4 for row in range(row_offset, self.grid_layout.rowCount()): @@ -686,10 +686,10 @@ def update_layout(self): else: self.scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) - def set_title(self, title: str): + def set_title(self, title: str) -> None: self.group_box.setTitle(title) - def update_markers(self): + def update_markers(self) -> None: self.translation_parameters = {} self.rotation_parameters = {} for key, transformation_matrix in self.markers.items(): @@ -697,11 +697,11 @@ def update_markers(self): self.rotation_parameters[key] = self.parameters_from_rotation(transformation_matrix.rotation) self.update_layout() - def set_markers(self, markers: Dict[str, TransformationMatrix]): + def set_markers(self, markers: Dict[str, TransformationMatrix]) -> None: self.markers = markers self.update_markers() - def on_transform_format_changed(self): + def on_transform_format_changed(self) -> None: self.rotation_title_label.setText(self._rotation_label_text()) self.update_markers() @@ -711,11 +711,11 @@ def update_from_transformation_matrix(self) -> None: @classmethod def MarkersInCameraFrame( - cls, + cls: type["MarkerPosesWidget"], eye_in_hand: bool, display_mode: PoseWidgetDisplayMode = PoseWidgetDisplayMode.Basic, initial_rotation_information: RotationInformation = RotationInformation(), - ): + ) -> "MarkerPosesWidget": eih_camera_object_pose_img_path = get_image_file_path( "hand-eye-robot-and-calibration-board-camera-on-robot-camera-object-pose-low-res.png" ) @@ -733,11 +733,11 @@ def MarkersInCameraFrame( @classmethod def MarkersInRobotFrame( - cls, + cls: type["MarkerPosesWidget"], eye_in_hand: bool, display_mode: PoseWidgetDisplayMode = PoseWidgetDisplayMode.Basic, initial_rotation_information: RotationInformation = RotationInformation(), - ): + ) -> "MarkerPosesWidget": eih_robot_object_pose_img_path = get_image_file_path( "hand-eye-robot-and-calibration-board-camera-on-robot-robot-object-pose-low-res.png" ) diff --git a/modules/zividsamples/gui/widgets/show_yaml_dialog.py b/modules/zividsamples/gui/widgets/show_yaml_dialog.py index 3f62daba..8ccb98d4 100644 --- a/modules/zividsamples/gui/widgets/show_yaml_dialog.py +++ b/modules/zividsamples/gui/widgets/show_yaml_dialog.py @@ -22,7 +22,7 @@ def show_yaml_dialog(yaml_path: Path, title: str) -> None: dialog.setLayout(layout) - def adjust_dialog_size(): + def adjust_dialog_size() -> None: text_edit.document().adjustSize() document_size = text_edit.document().size().toSize() diff --git a/modules/zividsamples/gui/widgets/tab_content_widget.py b/modules/zividsamples/gui/widgets/tab_content_widget.py index acfeb696..0816374d 100644 --- a/modules/zividsamples/gui/widgets/tab_content_widget.py +++ b/modules/zividsamples/gui/widgets/tab_content_widget.py @@ -1,6 +1,8 @@ from pathlib import Path +from typing import Optional from PyQt5.QtWidgets import QWidget +from zividsamples.gui.wizard.data_directory import SessionInfo class TabContentWidget(QWidget): @@ -8,14 +10,14 @@ class TabContentWidget(QWidget): has_pending_changes: bool _is_current_tab: bool - def __init__(self, data_directory: Path, parent=None): + def __init__(self, data_directory: Path, parent: Optional[QWidget] = None) -> None: super().__init__(parent) self._is_current_tab = False self.data_directory = data_directory self.session_info = None self.has_pending_changes = True - def update_data_directory(self, data_directory: Path, session_info=None): + def update_data_directory(self, data_directory: Path, session_info: Optional[SessionInfo] = None) -> None: self.session_info = session_info if self.data_directory != data_directory: self.data_directory = data_directory @@ -24,17 +26,24 @@ def update_data_directory(self, data_directory: Path, session_info=None): def data_directory_has_data(self) -> bool: return any(entry.is_file() for entry in self.data_directory.iterdir()) - def is_current_tab(self): - """Returns True if this tab is currently visible.""" + def is_current_tab(self) -> bool: + """Returns True if this tab is currently visible. + + Returns: + True if this tab is currently visible. + """ return self._is_current_tab - def notify_current_tab(self, widget: QWidget): + def notify_current_tab(self, widget: QWidget) -> None: """Called by the parent to notify this widget which tab is currently visible. Pending changes are processed immediately regardless of whether this tab is current. Since loading happens in a background thread, all tabs can load in parallel. The parent is expected to call the current tab first so it gets priority. + + Args: + widget: The widget that is currently the visible tab. """ is_current = widget is self self._is_current_tab = is_current @@ -43,24 +52,35 @@ def notify_current_tab(self, widget: QWidget): self.on_pending_changes() self.has_pending_changes = False - def on_tab_visibility_changed(self, is_current: bool): - """ - Override in subclasses to handle tab visibility changes. - :param is_current: True if this tab is now visible, False otherwise. + def on_tab_visibility_changed(self, is_current: bool) -> None: + """Override in subclasses to handle tab visibility changes. We assume that any pending changes should be handled when the tab becomes visible. + + Args: + is_current: True if this tab is now visible, False otherwise. + + Raises: + NotImplementedError: If not implemented by a subclass. """ raise NotImplementedError("Subclasses should implement this method.") def is_loading(self) -> bool: - """Override in subclasses that perform background loading.""" + """Override in subclasses that perform background loading. + + Returns: + True if this tab is currently loading data in the background. + """ return False - def on_pending_changes(self): + def on_pending_changes(self) -> None: """Override in subclasses to handle pending changes. Called whenever the data directory changes, for all tabs (not only the currently visible one). Implementations should clear stale in-memory data before loading to avoid showing unnecessary confirmation dialogs. + + Raises: + NotImplementedError: If not implemented by a subclass. """ raise NotImplementedError("Subclasses should implement this method.") diff --git a/modules/zividsamples/gui/widgets/tab_with_robot_support.py b/modules/zividsamples/gui/widgets/tab_with_robot_support.py index 2a26ffe9..b2b06118 100644 --- a/modules/zividsamples/gui/widgets/tab_with_robot_support.py +++ b/modules/zividsamples/gui/widgets/tab_with_robot_support.py @@ -5,16 +5,28 @@ class TabWidgetWithRobotSupport(TabContentWidget): - def on_actual_pose_updated(self, robot_target: RobotTarget): + def on_actual_pose_updated(self, robot_target: RobotTarget) -> None: """ Override in subclasses to handle robot pose update. - :param robot_target: Actual robot pose now. + + Args: + robot_target: Actual robot pose now. + + Raises: + NotImplementedError: If not implemented by a subclass. """ raise NotImplementedError("Subclasses should implement this method.") - def rotation_format_update(self, rotation_information: RotationInformation): + def rotation_format_update(self, rotation_information: RotationInformation) -> None: """ Override in subclasses to handle rotation information updates. + This is called when user updates rotation format. + + Args: + rotation_information: Rotation format selected by the user. + + Raises: + NotImplementedError: If not implemented by a subclass. """ raise NotImplementedError("Subclasses should implement this method.") diff --git a/modules/zividsamples/gui/widgets/tutorial_widget.py b/modules/zividsamples/gui/widgets/tutorial_widget.py index 87eb7001..bb295910 100644 --- a/modules/zividsamples/gui/widgets/tutorial_widget.py +++ b/modules/zividsamples/gui/widgets/tutorial_widget.py @@ -1,4 +1,4 @@ -from typing import Dict, List +from typing import Dict, List, Optional from PyQt5.QtWidgets import QGroupBox, QTextBrowser, QVBoxLayout, QWidget @@ -10,8 +10,8 @@ class TutorialWidget(QWidget): def __init__( self, - parent=None, - ): + parent: Optional[QWidget] = None, + ) -> None: super().__init__(parent) self.group_box = QGroupBox("Tutorial", self) @@ -31,23 +31,23 @@ def __init__( self.set_text_margins(25, 25, 25, 25) - def set_title(self, title: str): + def set_title(self, title: str) -> None: self.title = title self.update_text() - def set_description(self, description: List[str]): + def set_description(self, description: List[str]) -> None: self.description = description self.update_text() - def clear_steps(self): + def clear_steps(self) -> None: self.steps.clear() self.update_text() - def add_steps(self, steps: Dict[str, bool]): + def add_steps(self, steps: Dict[str, bool]) -> None: self.steps.update(steps) self.update_text() - def update_text(self): + def update_text(self) -> None: self.text_area.clear() text = f"

{self.title}

" text += "
" @@ -58,7 +58,7 @@ def update_text(self): text += "

" + "

".join(paragraph for paragraph in self.description) + "

" self.text_area.setHtml(text) - def set_text_margins(self, left, top, right, bottom): + def set_text_margins(self, left: int, top: int, right: int, bottom: int) -> None: document = self.text_area.document() document.setDocumentMargin(10) diff --git a/modules/zividsamples/gui/wizard/camera_selection.py b/modules/zividsamples/gui/wizard/camera_selection.py index a5b4d610..5c8a55b5 100644 --- a/modules/zividsamples/gui/wizard/camera_selection.py +++ b/modules/zividsamples/gui/wizard/camera_selection.py @@ -20,25 +20,25 @@ class FirmwareUpdateWorker(QObject): finished = pyqtSignal() error = pyqtSignal(str) - def __init__(self, camera): + def __init__(self, camera: zivid.Camera) -> None: super().__init__() self.camera = camera @pyqtSlot() - def run(self): + def run(self) -> None: try: zivid.firmware.update(self.camera, self._progress_callback) self.finished.emit() except Exception as e: self.error.emit(str(e)) - def _progress_callback(self, progress: float, description: str): + def _progress_callback(self, progress: float, description: str) -> None: self.progress.emit(progress, description) class CameraSelectionDialog(QDialog): - def __init__(self, zivid_app: zivid.Application, connect: bool): + def __init__(self, zivid_app: zivid.Application, connect: bool) -> None: super().__init__() self.selected_camera: Optional[zivid.Camera] = None self.firmware_updater_worker: Optional[FirmwareUpdateWorker] = None @@ -50,7 +50,7 @@ def __init__(self, zivid_app: zivid.Application, connect: bool): QTimer.singleShot(0, self.find_cameras) - def init_ui(self): + def init_ui(self) -> None: self.setWindowTitle("Select a Camera") layout = QVBoxLayout(self) @@ -65,11 +65,11 @@ def init_ui(self): self.setLayout(layout) - def find_cameras(self): + def find_cameras(self) -> None: self.camera_list_widget.addItem("Finding cameras...") QTimer.singleShot(100, self.update_camera_list) - def update_camera_list(self): + def update_camera_list(self) -> None: cameras = self.zivid_app.cameras() self.camera_list_widget.clear() camera_selected = False @@ -91,7 +91,7 @@ def update_camera_list(self): self.select_button.setText("Ok") QTimer.singleShot(0, self.adjust_dialog_size) - def adjust_dialog_size(self): + def adjust_dialog_size(self) -> None: max_width = 0 for index in range(self.camera_list_widget.count()): item = self.camera_list_widget.item(index) @@ -103,7 +103,7 @@ def adjust_dialog_size(self): dialog_size = QSize(max_width, self.sizeHint().height()) self.resize(dialog_size.expandedTo(QSize(300, 200))) - def on_firmware_update_progress(self, progress: float, description: str): + def on_firmware_update_progress(self, progress: float, description: str) -> None: last_item = self.camera_list_widget.item(self.camera_list_widget.count() - 1) if self.last_description != "" and last_item.text().endswith(f"({description})"): last_item.setText(f"{progress:6.2f}% ({description})") @@ -112,16 +112,16 @@ def on_firmware_update_progress(self, progress: float, description: str): self.last_description = description self.camera_list_widget.scrollToBottom() - def on_firmware_update_finished(self): + def on_firmware_update_finished(self) -> None: self.camera_list_widget.addItem("Firmware update complete.") self.camera_list_widget.addItem("Connecting...") QTimer.singleShot(100, lambda: self.connect_camera(self.selected_camera)) - def on_firmware_update_error(self, error_message: str): + def on_firmware_update_error(self, error_message: str) -> None: QMessageBox.critical(self, "Firmware Update Error", error_message) QTimer.singleShot(100, self.find_cameras) - def setup_firmware_update_thread(self, camera: zivid.Camera): + def setup_firmware_update_thread(self, camera: zivid.Camera) -> None: if self.firmware_updater_thread and not self.firmware_updater_thread.isRunning(): self.firmware_updater_thread = None @@ -141,7 +141,7 @@ def setup_firmware_update_thread(self, camera: zivid.Camera): self.firmware_updater_worker.error.connect(self.firmware_updater_thread.quit) self.firmware_updater_worker.error.connect(self.firmware_updater_worker.deleteLater) - def update_firmware(self, camera: zivid.Camera): + def update_firmware(self, camera: zivid.Camera) -> None: if camera is not None: try: self.select_button.setText("Updating Firmware...") @@ -157,7 +157,7 @@ def update_firmware(self, camera: zivid.Camera): self, "Connection Error", f"Failed to connect to camera {camera.info.serial_number}: {str(e)}" ) - def connect_camera(self, camera: Optional[zivid.Camera]): + def connect_camera(self, camera: Optional[zivid.Camera]) -> None: if camera is not None: try: camera.connect() @@ -169,7 +169,7 @@ def connect_camera(self, camera: Optional[zivid.Camera]): self, "Connection Error", f"Failed to connect to camera {camera.info.serial_number}: {str(e)}" ) - def on_select(self): + def on_select(self) -> None: self.select_button.setChecked(True) self.select_button.setStyleSheet("background-color: yellow;") QApplication.processEvents() @@ -211,7 +211,7 @@ def on_select(self): else: self.accept() - def accept(self): + def accept(self) -> None: self.select_button.setChecked(False) self.select_button.setStyleSheet("") super().accept() diff --git a/modules/zividsamples/gui/wizard/data_directory.py b/modules/zividsamples/gui/wizard/data_directory.py index 7afd71f9..4a1f28ce 100644 --- a/modules/zividsamples/gui/wizard/data_directory.py +++ b/modules/zividsamples/gui/wizard/data_directory.py @@ -3,10 +3,12 @@ from dataclasses import dataclass from datetime import datetime from pathlib import Path -from typing import Any, Dict, List, Optional +from types import TracebackType +from typing import Any, Dict, List, Optional, Type from PyQt5 import sip from PyQt5.QtCore import QSettings, Qt, pyqtSignal +from PyQt5.QtGui import QMouseEvent from PyQt5.QtWidgets import ( QCheckBox, QDialog, @@ -19,6 +21,7 @@ QPushButton, QStyle, QVBoxLayout, + QWidget, ) from zividsamples.gui.qt_application import ZividQtApplication, create_horizontal_line @@ -54,15 +57,15 @@ class SessionInfo: name: str root_folder: Path - def __post_init__(self): + def __post_init__(self) -> None: self._extra_data: Dict[str, Any] = {} self._ensure_structure() - def _ensure_structure(self): + def _ensure_structure(self) -> None: for sub in SESSION_FOLDER_STRUCTURE.values(): (self.root_folder / self.name / sub).mkdir(parents=True, exist_ok=True) - def update_last_modified_date(self): + def update_last_modified_date(self) -> None: self.last_modified_date = datetime.now().strftime(DATE_FORMAT) def has_any_data(self) -> bool: @@ -72,10 +75,10 @@ def has_any_data(self) -> bool: return True return False - def get_section(self, key: str) -> Optional[Any]: + def get_section(self, key: str) -> Optional[Dict[str, Any]]: return self._extra_data.get(key) - def set_section(self, key: str, value: Any) -> None: + def set_section(self, key: str, value: Dict[str, Any]) -> None: self._extra_data[key] = value def to_dict(self) -> dict: @@ -94,7 +97,7 @@ def save(self) -> None: json.dump(self.to_dict(), session_file) @classmethod - def from_existing(cls, root_folder: Path, session_name: str) -> "SessionInfo": + def from_existing(cls: Type["SessionInfo"], root_folder: Path, session_name: str) -> "SessionInfo": with open(root_folder / session_name / "session_info.json", "r", encoding="utf-8") as session_file: session_data = json.load(session_file) core_keys = {"last_modified_date", "created_date", "name"} @@ -108,7 +111,7 @@ def from_existing(cls, root_folder: Path, session_name: str) -> "SessionInfo": return instance @classmethod - def new(cls, root_folder: Path) -> "SessionInfo": + def new(cls: Type["SessionInfo"], root_folder: Path) -> "SessionInfo": date_string = datetime.now().strftime(DATE_FORMAT) return cls( last_modified_date=date_string, @@ -122,7 +125,7 @@ class DataDirectory: root_folder: Path session: Optional[SessionInfo] - def __init__(self): + def __init__(self) -> None: qsettings = QSettings("Zivid", "HandEyeGUI") qsettings.beginGroup("data_directory") self.root_folder = Path(qsettings.value("root_folder", str(Path.cwd()), type=str)) @@ -159,10 +162,10 @@ def widget_names_with_data_in_session(self, session: SessionInfo) -> List[str]: widget_names.append(name) return widget_names - def __str__(self): + def __str__(self) -> str: return f"DataDirectory(root_folder={self.root_folder}, session={self.session}, show_on_startup={self.show_on_startup})" - def save_choice(self): + def save_choice(self) -> None: qsettings = QSettings("Zivid", "HandEyeGUI") qsettings.beginGroup("data_directory") qsettings.setValue("root_folder", str(self.root_folder)) @@ -178,11 +181,11 @@ def save_choice(self): class ClickableLineEdit(QLineEdit): clicked = pyqtSignal() - def __init__(self, text="", parent=None): + def __init__(self, text: str = "", parent: Optional[QWidget] = None) -> None: super().__init__(text, parent) self.setReadOnly(True) - def mousePressEvent(self, a0): + def mousePressEvent(self, a0: Optional[QMouseEvent]) -> None: # Handle click (e.g., open folder dialog) self.clicked.emit() super().mousePressEvent(a0) @@ -191,7 +194,7 @@ def mousePressEvent(self, a0): class DirectoryAndSessionDialog(QDialog): SESSION_UNMODIFIED = 1 - def __init__(self, current_data_directory: DataDirectory, parent=None): + def __init__(self, current_data_directory: DataDirectory, parent: Optional[QWidget] = None) -> None: super().__init__(parent) self.setWindowTitle("Choose Data Directory and Session") self.selected_session = None @@ -201,7 +204,7 @@ def __init__(self, current_data_directory: DataDirectory, parent=None): self.setup_layout() self.connect_signals() - def setup_widgets(self): + def setup_widgets(self) -> None: self.directory_edit = ClickableLineEdit(str(self.data_directory.root_folder), self) self.directory_edit.setMinimumWidth(400) self.directory_icon = QPushButton(self) @@ -231,7 +234,7 @@ def setup_widgets(self): self.load_session_button.setEnabled(self.list_widget.count() > 0) self.new_session_button = QPushButton("New Session", self) - def setup_layout(self): + def setup_layout(self) -> None: layout = QVBoxLayout(self) path_layout = QHBoxLayout() @@ -250,13 +253,13 @@ def setup_layout(self): button_layout.addWidget(self.new_session_button) layout.addLayout(button_layout) - def connect_signals(self): + def connect_signals(self) -> None: self.directory_edit.clicked.connect(self._choose_folder) self.directory_icon.clicked.connect(self._choose_folder) self.load_session_button.clicked.connect(self._choose_session) self.new_session_button.clicked.connect(self.accept) - def adjust_dialog_width(self): + def adjust_dialog_width(self) -> None: if not self.list_widget or sip.isdeleted(self.list_widget): return max_width = self.list_widget.sizeHintForColumn(0) + 2 * self.list_widget.frameWidth() @@ -267,7 +270,7 @@ def adjust_dialog_width(self): def root_folder(self) -> Path: return Path(self.directory_edit.text()) - def _choose_folder(self): + def _choose_folder(self) -> None: folder = QFileDialog.getExistingDirectory(self, "Select Data Directory", self.directory_edit.text()) if folder: self.directory_edit.setText(folder) @@ -277,7 +280,7 @@ def _choose_folder(self): self.set_session_list() self.load_session_button.setEnabled(self.list_widget.count() > 0) - def set_session_list(self): + def set_session_list(self) -> None: self.list_widget.clear() current_item = None for session in self.data_directory.existing_sessions().values(): @@ -299,13 +302,13 @@ def set_session_list(self): else: self.list_widget.setCurrentRow(0) - def _choose_session(self): + def _choose_session(self) -> None: current_item = self.list_widget.currentItem() if current_item is not None: self.selected_session = current_item.data(self.SESSION_UNMODIFIED) self.accept() - def done(self, a0): + def done(self, a0: int) -> None: model = self.list_widget.model() try: model.rowsInserted.disconnect(self.adjust_dialog_width) @@ -317,11 +320,11 @@ def done(self, a0): class DataDirectoryManager: - def __init__(self): + def __init__(self) -> None: self.data_directory = DataDirectory() - self.tab_widgets = {} + self.tab_widgets: Dict[str, QWidget] = {} - def register_tab_widget(self, widget, name: str): + def register_tab_widget(self, widget: QWidget, name: str) -> None: assert name not in self.tab_widgets, f"Widget with name {name} already registered." assert ( name in SESSION_FOLDER_STRUCTURE @@ -330,18 +333,18 @@ def register_tab_widget(self, widget, name: str): if hasattr(widget, "session_info"): widget.session_info = self.data_directory.session - def start_new_session(self): + def start_new_session(self) -> None: self.close_session() self.data_directory.session = SessionInfo.new(self.data_directory.root_folder) self.data_directory.save_choice() self._notify_tab_widgets() - def _notify_tab_widgets(self): + def _notify_tab_widgets(self) -> None: for widget in self.tab_widgets.values(): if hasattr(widget, "update_data_directory"): widget.update_data_directory(self.folder(widget.objectName()), session_info=self.data_directory.session) - def select_folder(self): + def select_folder(self) -> None: dialog = DirectoryAndSessionDialog(self.data_directory) if dialog.exec_() == QDialog.Accepted: self.data_directory.root_folder = dialog.root_folder() @@ -355,7 +358,7 @@ def select_folder(self): self.data_directory.save_choice() self._notify_tab_widgets() - def folder(self, widget_name) -> Path: + def folder(self, widget_name: str) -> Path: if not self.data_directory.session: raise RuntimeError("No session selected. Did you forget to call start_new_session() or select_folder()?") path = ( @@ -370,17 +373,22 @@ def folder(self, widget_name) -> Path: def show_on_startup(self) -> bool: return self.data_directory.show_on_startup - def close_session(self): + def close_session(self) -> None: if self.data_directory.session and not self.data_directory.session.has_any_data(): shutil.rmtree(self.data_directory.root_folder / self.data_directory.session.name, ignore_errors=True) self.data_directory.session = None self.data_directory.save_choice() - def __enter__(self): + def __enter__(self) -> "DataDirectoryManager": return self - def __exit__(self, exception_type, exception_value, traceback): + def __exit__( + self, + exception_type: Optional[Type[BaseException]], + exception_value: Optional[BaseException], + traceback: Optional[TracebackType], + ) -> None: self.close_session() diff --git a/modules/zividsamples/gui/wizard/hand_eye_configuration.py b/modules/zividsamples/gui/wizard/hand_eye_configuration.py index 9d53431f..f1f3ac6c 100644 --- a/modules/zividsamples/gui/wizard/hand_eye_configuration.py +++ b/modules/zividsamples/gui/wizard/hand_eye_configuration.py @@ -42,7 +42,7 @@ def __init__(self, *, eye_in_hand: Optional[bool] = None, calibration_object: Op self.show_dialog = settings.value("show_dialog", True, type=bool) settings.endGroup() - def save_choice(self): + def save_choice(self) -> None: settings = QSettings("Zivid", "HandEyeGUI") settings.beginGroup("hand_eye_configuration") settings.setValue("eye_in_hand", self.eye_in_hand) @@ -62,7 +62,7 @@ def __init__( initial_hand_eye_configuration: HandEyeConfiguration, show_calibration_object_selection: bool = True, show_eye_in_hand_selection: bool = True, - parent=None, + parent: Optional[QWidget] = None, ): super().__init__(parent) self.hand_eye_configuration = initial_hand_eye_configuration @@ -126,13 +126,13 @@ def __init__( self.setLayout(layout) - def set_hand_eye_configuration(self, updated_hand_eye_configuration: HandEyeConfiguration): + def set_hand_eye_configuration(self, updated_hand_eye_configuration: HandEyeConfiguration) -> None: self.checkerboard_object_radio_button.setChecked( updated_hand_eye_configuration.calibration_object == CalibrationObject.Checkerboard ) self.eye_in_hand_radio_button.setChecked(updated_hand_eye_configuration.eye_in_hand) - def update_hand_eye_configuration(self): + def update_hand_eye_configuration(self) -> None: if self.calibration_object_selection_active: self.hand_eye_configuration.calibration_object = ( CalibrationObject.Checkerboard @@ -143,10 +143,10 @@ def update_hand_eye_configuration(self): self.hand_eye_configuration.eye_in_hand = self.eye_in_hand_radio_button.isChecked() self.hand_eye_configuration_updated.emit(self.hand_eye_configuration) - def on_eye_in_hand_toggled(self, _: bool): + def on_eye_in_hand_toggled(self, _: bool) -> None: self.update_hand_eye_configuration() - def on_calibration_object_toggled(self, _: bool): + def on_calibration_object_toggled(self, _: bool) -> None: self.update_hand_eye_configuration() def eye_in_hand(self) -> bool: @@ -161,7 +161,7 @@ class HandEyeConfigurationSelection(QDialog): def __init__( self, initial_hand_eye_configuration: HandEyeConfiguration = HandEyeConfiguration(), - parent=None, + parent: Optional[QWidget] = None, ): super().__init__(parent) self.setWindowTitle("Select Hand-Eye Configuration") @@ -181,7 +181,7 @@ def __init__( layout.addWidget(self.remember_choice_checkbox) self.setLayout(layout) - def accept(self): + def accept(self) -> None: self.hand_eye_configuration = self.hand_eye_buttons.hand_eye_configuration self.hand_eye_configuration.show_dialog = self.remember_choice_checkbox.isChecked() self.hand_eye_configuration.save_choice() diff --git a/modules/zividsamples/gui/wizard/marker_configuration.py b/modules/zividsamples/gui/wizard/marker_configuration.py index 01ccc7a6..0e75837a 100644 --- a/modules/zividsamples/gui/wizard/marker_configuration.py +++ b/modules/zividsamples/gui/wizard/marker_configuration.py @@ -1,5 +1,5 @@ import re -from typing import Dict, List, Optional +from typing import Dict, List, Optional, Tuple import numpy as np from nptyping import NDArray, Shape, UInt8 @@ -38,7 +38,7 @@ class TouchMarkerWidget(QWidget): marker_id: int = 1 marker_dictionary: str = MarkerDictionary.aruco4x4_50 - def __init__(self, parent=None): + def __init__(self, parent: Optional[QWidget] = None) -> None: super().__init__(parent) self.marker_id_selection = QSpinBox() @@ -64,10 +64,10 @@ def __init__(self, parent=None): self.marker_id_selection.valueChanged.connect(self.on_marker_id_changed) self.marker_dictionary_selection.currentIndexChanged.connect(self.on_marker_dictionary_changed) - def on_marker_id_changed(self): + def on_marker_id_changed(self) -> None: self.marker_id = self.marker_id_selection.value() - def on_marker_dictionary_changed(self): + def on_marker_dictionary_changed(self) -> None: self.marker_dictionary = self.marker_dictionary_selection.currentText() self.marker_id = self.marker_id_selection.value() if self.marker_id > MarkerDictionary.marker_count(self.marker_dictionary): @@ -81,7 +81,7 @@ def get_tab_widgets_in_order(self) -> List[QWidget]: class MarkerListValidator(QValidator): - def validate(self, text_to_validate, pos): + def validate(self, text_to_validate: str, pos: int) -> Tuple[QValidator.State, str, int]: # Allow partial matches while typing, including ranges partial_regex = QRegExp(r"^\s*\d*(\s*-\s*\d*)?(\s*,\s*\d*(\s*-\s*\d*)?)*$") complete_regex = QRegExp(r"^\s*\d+\s*(\s*-\s*\d+)?(\s*,\s*\d+\s*(\s*-\s*\d+)?)*$") @@ -92,7 +92,7 @@ def validate(self, text_to_validate, pos): return QValidator.Intermediate, text_to_validate, pos return QValidator.Invalid, text_to_validate, pos - def fixup(self, text_to_fix): + def fixup(self, text_to_fix: str) -> str: # Allow digits, commas, and dashes, remove duplicates and sort fixed_input = "".join(c for c in text_to_fix if c.isdigit() or c in ",-") fixed_input = re.sub(r"([,-])(?:[,-])+", r"\1", fixed_input).strip(",").strip("-") @@ -144,7 +144,7 @@ def __init__(self, *, id_list: Optional[List[int]] = None, dictionary: Optional[ self.dictionary = settings.value("marker_configuration.dictionary", MarkerDictionary.aruco4x4_50, type=str) self.show_dialog = settings.value("marker_configuration.show_dialog", True, type=bool) - def save_choice(self): + def save_choice(self) -> None: settings = QSettings("Zivid", "HandEyeGUI") settings.setValue("marker_configuration.id_list", ",".join(map(str, self.id_list))) settings.setValue("marker_configuration.dictionary", self.dictionary) @@ -159,8 +159,8 @@ def __init__( self, initial_marker_configuration: MarkerConfiguration = MarkerConfiguration(), show_marker_image: bool = True, - parent=None, - ): + parent: Optional[QWidget] = None, + ) -> None: super().__init__(parent) self.marker_configuration = initial_marker_configuration @@ -190,14 +190,14 @@ def __init__( overall_layout.addWidget(self.marker_image) self.setLayout(overall_layout) - def on_marker_list_edited(self): + def on_marker_list_edited(self) -> None: validator = self.marker_list_line_edit.validator() text = self.marker_list_line_edit.text() fixed_text = validator.fixup(text) self.marker_list_line_edit.setText(fixed_text) self.marker_configuration.id_list = self.expand_ranges(fixed_text) - def on_marker_dictionary_changed(self): + def on_marker_dictionary_changed(self) -> None: self.marker_configuration.dictionary = self.marker_dictionary_selection.currentText() def set_pixmap(self, pixmap: QPixmap, reset_zoom: bool = False) -> None: @@ -213,7 +213,7 @@ def set_markers( rgba: NDArray[Shape["N, M, 4"], UInt8], # type: ignore pixel_mapping: PixelMapping, reset_zoom: bool = False, - ): + ) -> None: rgb = rgba[:, :, :3].copy().astype(np.uint8) rgba[:, :, :3] = self.cv2_handler.draw_detected_markers(markers, rgb, pixel_mapping) qimage_rgba = QImage( @@ -224,7 +224,7 @@ def set_markers( ) self.set_image(qimage_rgba, reset_zoom) - def setVisible(self, visible: bool): + def setVisible(self, visible: bool) -> None: super().setVisible(visible) self.marker_list_line_edit.setVisible(visible) self.marker_dictionary_selection.setVisible(visible) @@ -256,8 +256,8 @@ class MarkerConfigurationSelection(QDialog): def __init__( self, initial_marker_configuration: MarkerConfiguration = MarkerConfiguration(), - parent=None, - ): + parent: Optional[QWidget] = None, + ) -> None: super().__init__(parent) self.setWindowTitle("Select Marker Configuration") @@ -278,7 +278,7 @@ def __init__( layout.addWidget(self.remember_choice_checkbox) self.setLayout(layout) - def accept(self): + def accept(self) -> None: self.marker_configuration = self.marker_widget.marker_configuration self.marker_configuration.show_dialog = self.remember_choice_checkbox.isChecked() self.marker_configuration.save_choice() diff --git a/modules/zividsamples/gui/wizard/robot_configuration.py b/modules/zividsamples/gui/wizard/robot_configuration.py index b410ab9f..348ffc46 100644 --- a/modules/zividsamples/gui/wizard/robot_configuration.py +++ b/modules/zividsamples/gui/wizard/robot_configuration.py @@ -50,7 +50,7 @@ def robot_type(self) -> RobotEnum: return self._robot_type @robot_type.setter - def robot_type(self, value: RobotEnum): + def robot_type(self, value: RobotEnum) -> None: assert isinstance(value, RobotEnum) self._robot_type = value @@ -66,7 +66,7 @@ def can_get_pose(self) -> bool: def can_control(self) -> bool: return self.robot_type in [RobotEnum.ROBODK] - def save_choice(self): + def save_choice(self) -> None: qsettings = QSettings("Zivid", "HandEyeGUI") qsettings.beginGroup("robot_configuration") qsettings.setValue("robot_type", self._robot_type.value if self._robot_type else None) @@ -117,7 +117,7 @@ def __init__(self, initial_robot_configuration: RobotConfiguration = RobotConfig layout.addLayout(horizontal_layout) - def accept(self): + def accept(self) -> None: self.robot_configuration.robot_type = RobotEnum(self.robot_type_combo.currentText()) self.robot_configuration.ip_addr = self.ip_addr_edit.text() self.robot_configuration.show_dialog = self.show_dialog_checkbox.isChecked() diff --git a/modules/zividsamples/gui/wizard/rotation_format_configuration.py b/modules/zividsamples/gui/wizard/rotation_format_configuration.py index 90c8b56d..f2cf8181 100644 --- a/modules/zividsamples/gui/wizard/rotation_format_configuration.py +++ b/modules/zividsamples/gui/wizard/rotation_format_configuration.py @@ -36,7 +36,7 @@ class RotationFormats: rotation_matrix: RotationFormat = RotationFormat("Rotation Matrix", 9) @classmethod - def as_list(cls) -> List[RotationFormat]: + def as_list(cls: type["RotationFormats"]) -> List[RotationFormat]: fields_default = [] for field in fields(cls): assert isinstance(field.default, RotationFormat), f"Field {field} does not have {RotationFormat} type." @@ -44,7 +44,7 @@ def as_list(cls) -> List[RotationFormat]: return fields_default @classmethod - def from_name(cls, format_name: str) -> RotationFormat: + def from_name(cls: type["RotationFormats"], format_name: str) -> RotationFormat: for rotation_format in cls.as_list(): if rotation_format.name == format_name: return rotation_format @@ -87,7 +87,7 @@ def __init__( self.show_dialog = settings.value("show_dialog", True, type=bool) settings.endGroup() - def save_choice(self): + def save_choice(self) -> None: settings = QSettings("Zivid", "HandEyeGUI") settings.beginGroup("rotation_information") settings.setValue("rotation_format", self.rotation_format.name) @@ -108,7 +108,7 @@ class RotationFormatSelectionWidget(QWidget): rotation_information: RotationInformation rotation_format_update = pyqtSignal(RotationInformation) - def __init__(self, initial_rotation_information: RotationInformation, parent=None): + def __init__(self, initial_rotation_information: RotationInformation, parent: Optional[QWidget] = None) -> None: super().__init__(parent) self.rotation_information = initial_rotation_information @@ -119,7 +119,7 @@ def __init__(self, initial_rotation_information: RotationInformation, parent=Non self.show_euler_format_selector(self.rotation_information.rotation_format == RotationFormats.euler) - def setup_widgets(self): + def setup_widgets(self) -> None: self.format_selector_label = QLabel() self.format_selector_label.setText("Select Rotation Format") self.format_selector = QComboBox() @@ -149,7 +149,7 @@ def setup_widgets(self): self.radians_radio_button.setChecked(not self.rotation_information.use_degrees) self.degrees_radio_button.setChecked(self.rotation_information.use_degrees) - def setup_layout(self): + def setup_layout(self) -> None: self.grid_layout = QGridLayout() self.grid_layout.addWidget(self.format_selector_label, 0, 0) self.grid_layout.addWidget(self.format_selector, 0, 1, 1, 3) @@ -163,19 +163,19 @@ def setup_layout(self): self.setLayout(self.grid_layout) - def setup_connections(self): + def setup_connections(self) -> None: self.format_selector.currentIndexChanged.connect(self.on_transform_format_changed) self.euler_format_selector.currentIndexChanged.connect(self.on_transform_format_changed) self.extrinsic_radio_button.toggled.connect(self.on_transform_format_changed) self.degrees_radio_button.toggled.connect(self.on_transform_format_changed) - def show_euler_format_selector(self, show: bool): + def show_euler_format_selector(self, show: bool) -> None: self.euler_format_label.setVisible(show) self.euler_format_selector.setVisible(show) self.extrinsic_radio_button.setVisible(show) self.intrinsic_radio_button.setVisible(show) - def on_transform_format_changed(self): + def on_transform_format_changed(self) -> None: self.rotation_information.rotation_format = self.format_selector.currentData() self.rotation_information.extrinsic = self.extrinsic_radio_button.isChecked() text = self.euler_format_selector.currentText() @@ -193,7 +193,7 @@ def on_transform_format_changed(self): ) self.rotation_format_update.emit(self.rotation_information) - def set_rotation_format(self, rotation_information: RotationInformation): + def set_rotation_format(self, rotation_information: RotationInformation) -> None: for i in range(self.format_selector.count()): if self.format_selector.itemData(i) == rotation_information.rotation_format: self.format_selector.setCurrentIndex(i) @@ -251,7 +251,7 @@ class RobotInfo: class RotationFormatSelectionDialog(QDialog): - def __init__(self, initial_rotation_information: RotationInformation, parent=None): + def __init__(self, initial_rotation_information: RotationInformation, parent: Optional[QWidget] = None) -> None: super().__init__(parent) self.setWindowTitle("Select Rotation Format") self.rotation_format_selection_widget = RotationFormatSelectionWidget(initial_rotation_information) @@ -306,13 +306,13 @@ def __init__(self, initial_rotation_information: RotationInformation, parent=Non table.cellClicked.connect(self.on_row_selected) - def on_row_selected(self, row: int): + def on_row_selected(self, row: int) -> None: self.rotation_format_selection_widget.set_rotation_format(ListOfRobotFormats[row].rotation_information) def rotation_information(self) -> RotationInformation: return self.rotation_format_selection_widget.rotation_information - def accept(self): + def accept(self) -> None: rotation_information = self.rotation_information() rotation_information.show_dialog = self.show_dialog_checkbox.isChecked() rotation_information.save_choice() diff --git a/modules/zividsamples/gui/wizard/settings_selector.py b/modules/zividsamples/gui/wizard/settings_selector.py index ad397150..a2e54ba5 100644 --- a/modules/zividsamples/gui/wizard/settings_selector.py +++ b/modules/zividsamples/gui/wizard/settings_selector.py @@ -36,7 +36,7 @@ class SettingsPixelMappingIntrinsics: pixel_mapping: PixelMapping intrinsics: zivid.CameraIntrinsics - def save_settings(self, qsettings: QSettings): + def save_settings(self, qsettings: QSettings) -> None: qsettings.setValue("settings_2d3d", self.settings_2d3d.serialize()) qsettings.beginGroup("pixel_mapping") qsettings.setValue("row_stride", self.pixel_mapping.row_stride) @@ -47,7 +47,9 @@ def save_settings(self, qsettings: QSettings): qsettings.setValue("intrinsics", self.intrinsics.serialize()) @classmethod - def load_settings(cls, qsettings: QSettings) -> "SettingsPixelMappingIntrinsics": + def load_settings( + cls: type["SettingsPixelMappingIntrinsics"], qsettings: QSettings + ) -> "SettingsPixelMappingIntrinsics": settings_2d3d = ( zivid.Settings() if not qsettings.contains("settings_2d3d") @@ -92,7 +94,7 @@ def __init__( self.show_dialog = settings.value("show_dialog", True, type=bool) settings.endGroup() - def save_choice(self): + def save_choice(self) -> None: settings = QSettings("Zivid", "HandEyeGUI") settings.beginGroup("camera_settings") settings.beginGroup("production") @@ -313,12 +315,12 @@ def __init__(self, camera: zivid.Camera): self.setLayout(layout) - def get_settings(self): + def get_settings(self) -> zivid.Settings: engine = self.engine_selector.currentText() sampling_pixel = self.sampling_mode_selector.currentText() return _settings_for_hand_eye(self.camera, engine, sampling_pixel) - def reject(self): + def reject(self) -> None: engine = self.engine_selector.currentText() sampling_pixel = self.sampling_mode_selector.currentText() QMessageBox.critical( @@ -354,7 +356,7 @@ def __init__(self, camera: zivid.Camera): self.setLayout(layout) - def update_preset_selector(self, _: int): + def update_preset_selector(self, _: int) -> None: self.preset_selector.clear() presets = self.category_selector.currentData().presets for preset in presets: @@ -382,9 +384,9 @@ def __init__(self, camera: zivid.Camera): self.setLayout(layout) - def open_file_dialog(self): + def open_file_dialog(self) -> None: file_dialog = QFileDialog() - file_path, _ = file_dialog.getOpenFileName(self, "Select File", filter="YAML Files (*.yml *.yaml)") + file_path = file_dialog.getOpenFileName(self, "Select File", filter="YAML Files (*.yml *.yaml)")[0] if file_path: if validate_settings_file(self.camera, file_path): self.file_path_edit.setText(file_path) @@ -451,7 +453,7 @@ def next_page(self) -> bool: return True return False - def accept(self): + def accept(self) -> None: current_widget = self.stack.currentWidget() assert current_widget if self.stack.currentIndex() < 2: @@ -495,7 +497,7 @@ def accept(self): self.settings_for_hand_eye_gui.save_choice() super().accept() - def reject(self): + def reject(self) -> None: if self.next_page(): return self.manual_widget.reject() diff --git a/modules/zividsamples/gui/wizard/touch_configuration.py b/modules/zividsamples/gui/wizard/touch_configuration.py index fd3197c4..716182a1 100644 --- a/modules/zividsamples/gui/wizard/touch_configuration.py +++ b/modules/zividsamples/gui/wizard/touch_configuration.py @@ -8,6 +8,7 @@ from typing import List, Optional from PyQt5.QtCore import QSettings +from PyQt5.QtGui import QCloseEvent from PyQt5.QtWidgets import ( QComboBox, QFormLayout, @@ -42,7 +43,7 @@ def __init__( self.z_offset = qsettings.value("z_offset", 300, type=int) qsettings.endGroup() - def save_choice(self): + def save_choice(self) -> None: qsettings = QSettings("Zivid", "HandEyeGUI") qsettings.beginGroup("touch_configuration") qsettings.setValue("marker_id", self.marker_id) @@ -85,10 +86,10 @@ def __init__(self, initial_touch_configuration: TouchConfiguration = TouchConfig self.marker_dictionary_selection.currentIndexChanged.connect(self.on_marker_dictionary_changed) self.z_offset.valueChanged.connect(self.on_z_offset_changed) - def on_marker_id_changed(self): + def on_marker_id_changed(self) -> None: self.touch_configuration.marker_id = self.marker_id_selection.value() - def on_marker_dictionary_changed(self): + def on_marker_dictionary_changed(self) -> None: self.touch_configuration.marker_dictionary = self.marker_dictionary_selection.currentText() self.touch_configuration.marker_id = self.marker_id_selection.value() if self.touch_configuration.marker_id > MarkerDictionary.marker_count( @@ -100,10 +101,10 @@ def on_marker_dictionary_changed(self): 0, MarkerDictionary.marker_count(self.touch_configuration.marker_dictionary) - 1 ) - def on_z_offset_changed(self): + def on_z_offset_changed(self) -> None: self.touch_configuration.z_offset = self.z_offset.value() - def closeEvent(self, a0): + def closeEvent(self, a0: QCloseEvent) -> None: self.touch_configuration.save_choice() return super().closeEvent(a0) diff --git a/modules/zividsamples/save_load_transformation_matrix.py b/modules/zividsamples/save_load_transformation_matrix.py index a881603c..10e26657 100644 --- a/modules/zividsamples/save_load_transformation_matrix.py +++ b/modules/zividsamples/save_load_transformation_matrix.py @@ -10,9 +10,6 @@ def save_transformation_matrix(transformation_matrix: TransformationMatrix, yaml Args: transformation_matrix: TransformationMatrix to be saved. yaml_path: Path to the YAML file where the matrix will be saved. - - Raises: - RuntimeError: If the matrix is not affine. """ assert_affine_matrix_and_save(transformation_matrix.as_matrix(), yaml_path) diff --git a/modules/zividsamples/settings_utils.py b/modules/zividsamples/settings_utils.py index b0903a6e..96a839d5 100644 --- a/modules/zividsamples/settings_utils.py +++ b/modules/zividsamples/settings_utils.py @@ -17,6 +17,8 @@ def get_matching_2d_preset_settings( Args: camera: Zivid camera + sampling_color: Sampling color to match + sampling_pixel: Sampling pixel to match Raises: RuntimeError: If no matched preset is found diff --git a/requirements.txt b/requirements.txt index 2f2149c3..d7d9c5af 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ ./modules -open3d \ No newline at end of file +open3d; python_version < "3.13" \ No newline at end of file diff --git a/source/applications/advanced/barcode/barcode_detector.py b/source/applications/advanced/barcode/barcode_detector.py index 7a399c1d..0114beeb 100644 --- a/source/applications/advanced/barcode/barcode_detector.py +++ b/source/applications/advanced/barcode/barcode_detector.py @@ -27,33 +27,45 @@ def _main() -> None: LinearBarcodeFormat.ean8, LinearBarcodeFormat.upcA, LinearBarcodeFormat.upcE, + LinearBarcodeFormat.itf, } matrix_format_filter = {MatrixBarcodeFormat.qrcode, MatrixBarcodeFormat.dataMatrix} settings_2d = barcode_detector.suggest_settings(camera) - print("Detecting barcodes ...") + print("Capturing 2D frame ...") frame_2d = camera.capture_2d(settings_2d) - linear_barcode_results = barcode_detector.read_linear_codes(frame_2d, linear_format_filter) + print("Detecting linear barcode candidates ...") + detection_results = barcode_detector.detect_linear_codes(frame_2d) + + decoding_results = barcode_detector.decode_linear_codes(detection_results, linear_format_filter) + + if detection_results: + print(f"Detected {len(detection_results)} linear barcode candidates:") + for i, (candidate, decoded) in enumerate(zip(detection_results, decoding_results, strict=False)): + print(f"-- Candidate {i + 1}:") + print(f" Bounding box: {candidate.bounding_box()}") + if decoded is not None: + print(f" Code: {decoded.code()}") + print(f" Format: {decoded.code_format()}") + print(f" Bounding box: {decoded.bounding_box()}") + else: + print(" Failed to decode") + else: + print("No linear barcode candidates detected") + + print("Reading matrix barcodes ...") matrix_barcode_results = barcode_detector.read_matrix_codes(frame_2d, matrix_format_filter) - if linear_barcode_results: - print(f"Detected {len(linear_barcode_results)} linear barcodes:") - for result in linear_barcode_results: - print( - f"-- Detected barcode {result.code()} on format {result.code_format()} at pixel {result.center_position()}" - ) - if matrix_barcode_results: print(f"Detected {len(matrix_barcode_results)} matrix barcodes:") for result in matrix_barcode_results: - print( - f"-- Detected barcode {result.code()} on format {result.code_format()} at pixel {result.center_position()}" - ) - - if not linear_barcode_results and not matrix_barcode_results: - print("No barcodes detected") + print(f"-- Code: {result.code()}") + print(f" Format: {result.code_format()}") + print(f" Bounding box: {result.bounding_box()}") + else: + print("No matrix barcodes detected") if __name__ == "__main__": diff --git a/source/applications/advanced/cuda/capture_and_process_image_with_cupy_on_cuda.py b/source/applications/advanced/cuda/capture_and_process_image_with_cupy_on_cuda.py new file mode 100644 index 00000000..a68ccba9 --- /dev/null +++ b/source/applications/advanced/cuda/capture_and_process_image_with_cupy_on_cuda.py @@ -0,0 +1,116 @@ +""" +Demonstrate GPU interop with CuPy: wrap a Zivid GPU image buffer as a CuPy array without copying it through CPU memory. + +Only the handoff from Zivid to CuPy is zero-copy. CuPy operations afterwards (e.g. `arr * 1.2`, `.astype(...)`) allocate +new GPU buffers as usual, and `cp.asnumpy(...)` at the end copies to CPU for display. In a real pipeline you would keep +results on the GPU and pass them straight to the next consumer (PyTorch, OpenGL, a CUDA kernel, etc.) -- see the related +samples `capture_and_segment_image_with_pytorch_on_cuda.py` and `capture_and_render_point_cloud_with_opengl_on_cuda.py`. + +Requirements: +- CUDA-capable GPU +- CuPy installed: pip install cupy-cuda12x (adjust for your CUDA version) + +""" + +import zivid +from zividsamples.display import display_rgbs + +# CuPy import - this sample requires CuPy to be installed +try: + import cupy as cp +except ImportError: + print("⚠️ Failed to import CuPy. It is installed via `pip install cupy-cuda12x` (adjust for your CUDA version).") + raise + + +def _main() -> None: + app = zivid.Application() + + print("Verifying that CUDA backend is available") + compute_device = app.compute_device() + if compute_device.backend != zivid.ComputeBackend.cuda: + raise RuntimeError("This sample requires CUDA backend") + + print(f"Using GPU: {compute_device.model}") + + print("Connecting to camera") + camera = app.connect_camera() + + print("Configuring settings") + settings_2d = zivid.Settings2D(acquisitions=[zivid.Settings2D.Acquisition()]) + + print("Capturing 2D frame") + frame_2d = camera.capture_2d(settings_2d) + + # Use default CUDA stream for synchronization (simplest approach) + # For async processing, you can create a custom stream: + # cupy_stream = cp.cuda.Stream(non_blocking=True) + # cuda_stream = zivid.CUDAStreamPtr(cupy_stream.ptr) + cuda_stream = zivid.CUDAStreamPtr() # Default stream + + print("Getting GPU device buffer in float format (RGBAf32)") + # The DeviceArray is synchronized into cuda_stream at acquisition, so the + # device pointer below is a plain accessor that needs no further synchronization. + device_array = frame_2d.image_device_array(cuda_stream, zivid.PixelFormat.RGBAF) + + print("Getting device pointer") + device_ptr = device_array.device_pointer() + height = device_array.shape[0] + width = device_array.shape[1] + row_stride_bytes = device_array.strides_in_bytes[0] + row_stride_elements = device_array.strides[0] + total_size_bytes = device_array.size_bytes + + print(f"Device pointer: {hex(device_ptr)}") + print( + f"Buffer: {width}x{height}, stride={row_stride_bytes} bytes, {row_stride_elements} elements, total size={total_size_bytes} bytes" + ) + + print( + "Wrapping Zivid's GPU buffer as a CuPy memory pointer (this is the zero-copy step; Zivid still owns the memory)" + ) + # Note: RGBAf32 format = 4 channels of float32 + unowned_memory = cp.cuda.UnownedMemory(device_ptr, total_size_bytes, owner=device_array) + unowned_memory_ptr = cp.cuda.MemoryPointer(unowned_memory, 0) + + print("Creating array with correct shape accounting for stride") + row_elements = row_stride_elements + # pylint: disable-next=unexpected-keyword-arg + flat_array = cp.ndarray(shape=(height * row_elements,), dtype=cp.float32, memptr=unowned_memory_ptr) + + print("Reshaping array to image size (height, width, 4 channels)") + # If stride equals width*16, direct reshape is possible + if row_stride_bytes == width * 16: + image_array = flat_array.reshape((height, width, 4)) + else: + # Handling stride memory by slicing + image_array = flat_array.reshape((height, row_elements))[:, : width * 4].reshape((height, width, 4)) + + print(f"CuPy array shape: {image_array.shape}") + print(f"CuPy array dtype: {image_array.dtype}") + + print("Example: Computing mean color on GPU") + mean_rgba = cp.mean(image_array, axis=(0, 1)) + print(f"Mean RGBA values: R={mean_rgba[0]:.3f}, G={mean_rgba[1]:.3f}, B={mean_rgba[2]:.3f}, A={mean_rgba[3]:.3f}") + + print("Example: Increasing image brightness on GPU (allocates a new GPU buffer for the result)") + brightened = cp.clip(image_array * 1.2, 0.0, 1.0) + print("Applied brightness adjustment on GPU") + + print("Converting RGBAf32 to RGBA8 (0-255) for visualization") + original_image_gpu = (image_array * 255).astype("uint8") + brightened_image_gpu = (brightened * 255).astype("uint8") + + print("Copying result GPU -> CPU for display (in a real pipeline you would keep this on the GPU)") + original_image_cpu = cp.asnumpy(original_image_gpu) + brightened_image_cpu = cp.asnumpy(brightened_image_gpu) + print( + f"Copied to CPU array: original shape={original_image_cpu.shape}, brightened shape={brightened_image_cpu.shape}" + ) + + print("Displaying original and brightened images") + display_rgbs([original_image_cpu, brightened_image_cpu], ["original", "brightened"], layout=(1, 2)) + + +if __name__ == "__main__": + _main() diff --git a/source/applications/advanced/cuda/capture_and_render_point_cloud_with_opengl_on_cuda.py b/source/applications/advanced/cuda/capture_and_render_point_cloud_with_opengl_on_cuda.py new file mode 100644 index 00000000..f2a3942c --- /dev/null +++ b/source/applications/advanced/cuda/capture_and_render_point_cloud_with_opengl_on_cuda.py @@ -0,0 +1,688 @@ +""" +Demonstrate GPU point cloud rendering with CUDA-OpenGL interop: capture a Zivid point cloud, copy it device-to-device +into OpenGL vertex buffers, and render it interactively. + +The point cloud data never goes through CPU memory: Zivid produces it on the GPU as a DeviceArray, CUDA copies it +directly into the OpenGL VBOs (the interop step), and OpenGL renders from those buffers. The CUDA-OpenGL interop happens +at the `cudaMemcpyAsync` from the Zivid device pointer into the CUDA-registered OpenGL buffer. See the related samples +`capture_and_process_image_with_cupy_on_cuda.py` and `capture_and_segment_image_with_pytorch_on_cuda.py` for other +ways to consume Zivid GPU data without a CPU round-trip. + +Controls: +- Left-drag: orbit +- Scroll: zoom +- ESC: exit + +Requirements: +- CUDA-capable GPU +- glfw: pip install glfw +- PyOpenGL: pip install PyOpenGL + +""" + +from __future__ import annotations + +import ctypes +import math +import sys +from ctypes import byref, c_int, c_size_t, c_uint, c_void_p + +import numpy as np +import numpy.typing as npt +import zivid + +try: + import glfw +except ImportError: + print("⚠️ Failed to import glfw. It is installed via `pip install glfw`.") + raise + +try: + from OpenGL.GL import ( + GL_ARRAY_BUFFER, + GL_COLOR_BUFFER_BIT, + GL_COMPILE_STATUS, + GL_DEPTH_BUFFER_BIT, + GL_DEPTH_TEST, + GL_DYNAMIC_DRAW, + GL_FALSE, + GL_FLOAT, + GL_FRAGMENT_SHADER, + GL_LINK_STATUS, + GL_POINTS, + GL_PROGRAM_POINT_SIZE, + GL_TRUE, + GL_UNSIGNED_BYTE, + GL_VERTEX_SHADER, + glAttachShader, + glBindBuffer, + glBindVertexArray, + glBufferData, + glClear, + glClearColor, + glCompileShader, + glCreateProgram, + glCreateShader, + glDeleteBuffers, + glDeleteProgram, + glDeleteShader, + glDeleteVertexArrays, + glDrawArrays, + glEnable, + glEnableVertexAttribArray, + glGenBuffers, + glGenVertexArrays, + glGetProgramInfoLog, + glGetProgramiv, + glGetShaderInfoLog, + glGetShaderiv, + glGetUniformLocation, + glLinkProgram, + glShaderSource, + glUniformMatrix4fv, + glUseProgram, + glVertexAttribPointer, + glViewport, + ) +except ImportError: + print("⚠️ Failed to import PyOpenGL. It is installed via `pip install PyOpenGL`.") + raise + + +# CUDA Runtime ctypes bindings +_CUDA_SUCCESS = 0 +_CUDA_MEMCPY_DEVICE_TO_DEVICE = 3 +_CUDA_GRAPHICS_REGISTER_FLAGS_NONE = 0 + + +def _load_cuda_runtime(compute_device: zivid.ComputeDevice) -> _CudaRuntime: + """Load the CUDA runtime shared library matching the SDK's CUDA version. + + Args: + compute_device: Zivid ComputeDevice instance. + + Returns: + _CudaRuntime wrapper around the loaded library. + + """ + library_name = compute_device.cuda_runtime_library_name + if sys.platform == "win32": + library = ctypes.WinDLL(library_name) + else: + library = ctypes.CDLL(library_name) + return _CudaRuntime(library) + + +def _check_cuda(cuda_error: int, cuda_function_name: str) -> None: + """Check CUDA return code and raise on error. + + Args: + cuda_error: CUDA error code returned by a runtime API call. + cuda_function_name: Name of the CUDA function for the error message. + + Raises: + RuntimeError: If cuda_error is not cudaSuccess. + + """ + if cuda_error != _CUDA_SUCCESS: + raise RuntimeError(f"CUDA error {cuda_error} in {cuda_function_name}") + + +class _CudaRuntime: + """Thin wrapper around the CUDA runtime shared library loaded via ctypes. + + All CUDA-GL interop and memory copy operations go through this class. + """ + + def __init__(self, library: ctypes.CDLL) -> None: + self._library = library + + def graphics_gl_register_buffer(self, gl_buffer_id: int) -> c_void_p: + """Register an OpenGL buffer object with CUDA for interop. + + Args: + gl_buffer_id: OpenGL buffer name (GLuint). + + Returns: + Opaque CUDA graphics resource handle. + + """ + resource = c_void_p() # pylint: disable=no-value-for-parameter + cuda_error = self._library.cudaGraphicsGLRegisterBuffer( + byref(resource), + c_uint(gl_buffer_id), + c_uint(_CUDA_GRAPHICS_REGISTER_FLAGS_NONE), + ) + _check_cuda(cuda_error, "cudaGraphicsGLRegisterBuffer") + return resource + + def graphics_map_resources(self, resource: c_void_p, stream: int = 0) -> None: + """Map a CUDA graphics resource for access from CUDA. + + Args: + resource: Opaque CUDA graphics resource handle. + stream: CUDA stream pointer (0 for default stream). + + """ + resource_array = (c_void_p * 1)(resource) + cuda_error = self._library.cudaGraphicsMapResources(c_int(1), resource_array, c_void_p(stream)) + _check_cuda(cuda_error, "cudaGraphicsMapResources") + + def graphics_unmap_resources(self, resource: c_void_p, stream: int = 0) -> None: + """Unmap a CUDA graphics resource. + + Args: + resource: Opaque CUDA graphics resource handle. + stream: CUDA stream pointer (0 for default stream). + + """ + resource_array = (c_void_p * 1)(resource) + cuda_error = self._library.cudaGraphicsUnmapResources(c_int(1), resource_array, c_void_p(stream)) + _check_cuda(cuda_error, "cudaGraphicsUnmapResources") + + def graphics_resource_get_mapped_pointer(self, resource: c_void_p) -> tuple[int | None, int]: + """Get the device pointer for a mapped CUDA graphics resource. + + Args: + resource: Opaque CUDA graphics resource handle. + + Returns: + Tuple of (device_pointer, size_in_bytes). + + """ + device_pointer = c_void_p() # pylint: disable=no-value-for-parameter + size = c_size_t() # pylint: disable=no-value-for-parameter + cuda_error = self._library.cudaGraphicsResourceGetMappedPointer(byref(device_pointer), byref(size), resource) + _check_cuda(cuda_error, "cudaGraphicsResourceGetMappedPointer") + return device_pointer.value, size.value + + def memcpy_device_to_device(self, dst: int | None, src: int, size_bytes: int, stream: int = 0) -> None: + """Copy memory between two GPU device pointers. + + Args: + dst: Destination device pointer. + src: Source device pointer. + size_bytes: Number of bytes to copy. + stream: CUDA stream pointer (0 for default stream). + + """ + cuda_error = self._library.cudaMemcpyAsync( + c_void_p(dst), + c_void_p(src), + c_size_t(size_bytes), + c_int(_CUDA_MEMCPY_DEVICE_TO_DEVICE), + c_void_p(stream), + ) + _check_cuda(cuda_error, "cudaMemcpyAsync") + + def stream_synchronize(self, stream: int = 0) -> None: + """Block until all operations on the given CUDA stream have completed. + + Args: + stream: CUDA stream pointer (0 for default stream). + + """ + cuda_error = self._library.cudaStreamSynchronize(c_void_p(stream)) + _check_cuda(cuda_error, "cudaStreamSynchronize") + + def graphics_unregister_resource(self, resource: c_void_p) -> None: + """Unregister a CUDA graphics resource. + + Args: + resource: Opaque CUDA graphics resource handle. + + """ + self._library.cudaGraphicsUnregisterResource(resource) + + +# OpenGL shader helpers + +_VERTEX_SHADER_SOURCE = """ +#version 330 core +layout(location = 0) in vec4 position; +layout(location = 1) in vec4 color; + +uniform mat4 mvp; + +out vec4 frag_color; + +void main() { + gl_Position = mvp * vec4(position.xyz, 1.0); + frag_color = color; + gl_PointSize = 2.0; +} +""" + +_FRAGMENT_SHADER_SOURCE = """ +#version 330 core +in vec4 frag_color; +out vec4 out_color; + +void main() { + out_color = frag_color; +} +""" + + +def _compile_shader(source: str, shader_type: int) -> int: + """Compile a GLSL shader from source. + + Args: + source: GLSL source code string. + shader_type: GL_VERTEX_SHADER or GL_FRAGMENT_SHADER. + + Returns: + Compiled shader object. + + Raises: + RuntimeError: If compilation fails. + + """ + shader = glCreateShader(shader_type) + glShaderSource(shader, source) + glCompileShader(shader) + if not glGetShaderiv(shader, GL_COMPILE_STATUS): + info_log = glGetShaderInfoLog(shader).decode() + raise RuntimeError(f"Shader compilation failed:\n{info_log}") + return shader + + +def _create_shader_program() -> int: + """Create, compile, and link the point cloud shader program. + + Returns: + Linked shader program object. + + Raises: + RuntimeError: If linking fails. + + """ + vertex_shader = _compile_shader(_VERTEX_SHADER_SOURCE, GL_VERTEX_SHADER) + fragment_shader = _compile_shader(_FRAGMENT_SHADER_SOURCE, GL_FRAGMENT_SHADER) + program = glCreateProgram() + glAttachShader(program, vertex_shader) + glAttachShader(program, fragment_shader) + glLinkProgram(program) + if not glGetProgramiv(program, GL_LINK_STATUS): + info_log = glGetProgramInfoLog(program).decode() + raise RuntimeError(f"Shader link failed:\n{info_log}") + glDeleteShader(vertex_shader) + glDeleteShader(fragment_shader) + return program + + +def _perspective_matrix(fov_y_deg: float, aspect: float, near: float, far: float) -> np.ndarray: + """Create a perspective projection matrix. + + Args: + fov_y_deg: Vertical field of view in degrees. + aspect: Width / height aspect ratio. + near: Near clipping plane distance. + far: Far clipping plane distance. + + Returns: + 4x4 numpy float32 matrix. + + """ + f = 1.0 / math.tan(math.radians(fov_y_deg) / 2.0) + matrix = np.zeros((4, 4), dtype=np.float32) + matrix[0, 0] = f / aspect + matrix[1, 1] = f + matrix[2, 2] = (far + near) / (near - far) + matrix[2, 3] = (2.0 * far * near) / (near - far) + matrix[3, 2] = -1.0 + return matrix + + +def _look_at(eye: npt.ArrayLike, center: npt.ArrayLike, up: npt.ArrayLike) -> np.ndarray: + """Create a view matrix (right-handed, looking along -Z in eye space). + + Args: + eye: Camera position as 3-element array. + center: Look-at target as 3-element array. + up: Up direction as 3-element array. + + Returns: + 4x4 numpy float32 matrix. + + """ + eye = np.asarray(eye, dtype=np.float32) + center = np.asarray(center, dtype=np.float32) + up = np.asarray(up, dtype=np.float32) + + f = center - eye + f = f / np.linalg.norm(f) + s = np.cross(f, up) + s = s / np.linalg.norm(s) + u = np.cross(s, f) + + matrix = np.eye(4, dtype=np.float32) + matrix[0, :3] = s + matrix[1, :3] = u + matrix[2, :3] = -f + matrix[0, 3] = -np.dot(s, eye) + matrix[1, 3] = -np.dot(u, eye) + matrix[2, 3] = np.dot(f, eye) + return matrix + + +class _OrbitCamera: + """Orbit camera controlled by mouse drag and scroll. + + Orbits around a center point. Yaw rotates horizontally, pitch rotates + vertically. Scroll changes distance from the center. + + The Zivid coordinate system has Y pointing down and Z pointing into the + scene, so the initial view looks along +Z with Y-down mapped to screen-up. + + """ + + def __init__(self, center: list[float], distance: float) -> None: + self.center = np.array(center, dtype=np.float32) + self.distance = distance + # yaw=180 looks along +Z (into the scene, matching the capture viewpoint) + self.yaw = 180.0 + self.pitch = 0.0 + self._last_x = 0.0 + self._last_y = 0.0 + self._dragging = False + + def view_matrix(self) -> np.ndarray: + """Compute the view matrix for the current orbit state. + + Returns: + 4x4 numpy float32 view matrix. + + """ + yaw_rad = math.radians(self.yaw) + pitch_rad = math.radians(self.pitch) + + cos_pitch = math.cos(pitch_rad) + eye = self.center + self.distance * np.array( + [cos_pitch * math.sin(yaw_rad), math.sin(pitch_rad), cos_pitch * math.cos(yaw_rad)], + dtype=np.float32, + ) + + # Zivid Y points down; use (0,-1,0) as up so the image appears right-side-up + return _look_at(eye, self.center, [0.0, -1.0, 0.0]) + + def on_mouse_button(self, _window: object, button: int, action: int, _mods: int) -> None: + """GLFW mouse button callback. + + Args: + button: Mouse button identifier. + action: Press or release action. + + """ + if button == glfw.MOUSE_BUTTON_LEFT: + self._dragging = action == glfw.PRESS + if self._dragging: + self._last_x, self._last_y = glfw.get_cursor_pos(_window) + + def on_cursor_pos(self, _window: object, cursor_x: float, cursor_y: float) -> None: + """GLFW cursor position callback. + + Args: + cursor_x: Cursor X position. + cursor_y: Cursor Y position. + + """ + if self._dragging: + dx = cursor_x - self._last_x + dy = cursor_y - self._last_y + self.yaw += dx * 0.3 + self.pitch = max(-89.0, min(89.0, self.pitch - dy * 0.3)) + self._last_x = cursor_x + self._last_y = cursor_y + + def on_scroll(self, _window: object, _xoffset: float, yoffset: float) -> None: + """GLFW scroll callback. + + Args: + yoffset: Vertical scroll offset. + + """ + self.distance = max(10.0, self.distance * (1.0 - yoffset * 0.1)) + + +def _copy_device_array_to_vbo( + cuda_runtime: _CudaRuntime, + device_array: zivid.DeviceArray, + cuda_resource: c_void_p, +) -> None: + """Copy a DeviceArray into a CUDA-registered OpenGL VBO (device-to-device). + + Args: + cuda_runtime: _CudaRuntime instance. + device_array: Zivid DeviceArray already synchronized into the caller's stream. + Uses size_bytes for transfer size. + cuda_resource: CUDA graphics resource handle for this VBO. + + """ + src_ptr = device_array.device_pointer() + cuda_runtime.graphics_map_resources(cuda_resource) + dst_ptr = cuda_runtime.graphics_resource_get_mapped_pointer(cuda_resource)[0] + cuda_runtime.memcpy_device_to_device(dst_ptr, src_ptr, device_array.size_bytes) + cuda_runtime.graphics_unmap_resources(cuda_resource) + + +def _create_glfw_window() -> object: + """Initialize GLFW and create an OpenGL 3.3 core profile window. + + Returns: + GLFW window handle. + + Raises: + RuntimeError: If GLFW initialization or window creation fails. + + """ + if not glfw.init(): + raise RuntimeError("Failed to initialize GLFW") + + glfw.window_hint(glfw.CONTEXT_VERSION_MAJOR, 3) + glfw.window_hint(glfw.CONTEXT_VERSION_MINOR, 3) + glfw.window_hint(glfw.OPENGL_PROFILE, glfw.OPENGL_CORE_PROFILE) + glfw.window_hint(glfw.OPENGL_FORWARD_COMPAT, glfw.TRUE) + + window = glfw.create_window(1280, 960, "Zivid GPU Point Cloud", None, None) + if not window: + glfw.terminate() + raise RuntimeError("Failed to create GLFW window") + glfw.make_context_current(window) + return window + + +def _capture_unorganized_point_cloud(app: zivid.Application) -> zivid.UnorganizedPointCloud: + """Capture a 3D frame and return the unorganized point cloud. + + Args: + app: Zivid Application instance. + + Returns: + UnorganizedPointCloud with valid points. + + Raises: + RuntimeError: If the point cloud has no valid points. + + """ + print("Connecting to camera") + camera = app.connect_camera() + settings = zivid.Settings( + acquisitions=[zivid.Settings.Acquisition()], + color=zivid.Settings2D(acquisitions=[zivid.Settings2D.Acquisition()]), + ) + + print("Capturing 3D frame") + frame = camera.capture_2d_3d(settings) + unorganized_point_cloud = frame.point_cloud().to_unorganized_point_cloud() + print(f"Unorganized point cloud: {unorganized_point_cloud.size} valid points") + + if unorganized_point_cloud.size == 0: + raise RuntimeError("Point cloud has no valid points") + + return unorganized_point_cloud + + +def _create_vao_and_vbos( + num_points: int, xyzw_array: zivid.DeviceArray, rgba_array: zivid.DeviceArray +) -> tuple[int, int, int, int]: + """Create the GPU vertex buffers (VBOs) and bind them to the shader via a VAO. + + VBO (Vertex Buffer Object): a chunk of GPU memory holding raw per-vertex data. + This sample uses two VBOs: one for XYZW positions, one for RGBA colors. + + VAO (Vertex Array Object): an OpenGL object that stores how to interpret one + or more VBOs -- which buffer feeds which shader input, the data type, the + stride. Binding the VAO once per draw call restores all that wiring, so the + render loop only needs to bind the VAO and draw. + + Args: + num_points: Number of points in the cloud. + xyzw_array: DeviceArray for XYZW position data. + rgba_array: DeviceArray for RGBA color data. + + Returns: + Tuple of (shader_program, vao, position_vbo, color_vbo). + + """ + program = _create_shader_program() + + vao = glGenVertexArrays(1) + glBindVertexArray(vao) + + # Position VBO: XYZW = 4 floats = 16 bytes per point + position_vbo = int(glGenBuffers(1)) + glBindBuffer(GL_ARRAY_BUFFER, position_vbo) + glBufferData(GL_ARRAY_BUFFER, num_points * xyzw_array.strides_in_bytes[0], None, GL_DYNAMIC_DRAW) + # Attribute 0: position (matches layout(location = 0) in vertex shader) + glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, None) + glEnableVertexAttribArray(0) + + # Color VBO: RGBA = 4 uint8 = 4 bytes per point (normalized to [0,1] by GL) + color_vbo = int(glGenBuffers(1)) + glBindBuffer(GL_ARRAY_BUFFER, color_vbo) + glBufferData(GL_ARRAY_BUFFER, num_points * rgba_array.strides_in_bytes[0], None, GL_DYNAMIC_DRAW) + # Attribute 1: color (matches layout(location = 1) in vertex shader) + glVertexAttribPointer(1, 4, GL_UNSIGNED_BYTE, GL_TRUE, 0, None) + glEnableVertexAttribArray(1) + + glBindVertexArray(0) + return program, vao, position_vbo, color_vbo + + +def _render_loop(window: object, orbit: _OrbitCamera, mvp_uniform_location: int, vao: int, num_points: int) -> None: + """Run the interactive render loop until the window is closed or ESC is pressed. + + Args: + window: GLFW window handle. + orbit: Orbit camera for view matrix computation. + mvp_uniform_location: Uniform location for the MVP matrix in the shader. + vao: OpenGL vertex array object. + num_points: Number of points to draw. + + """ + while not glfw.window_should_close(window): + glfw.poll_events() + if glfw.get_key(window, glfw.KEY_ESCAPE) == glfw.PRESS: + break + + framebuffer_width, framebuffer_height = glfw.get_framebuffer_size(window) + if framebuffer_width == 0 or framebuffer_height == 0: + continue + + glViewport(0, 0, framebuffer_width, framebuffer_height) + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) + + projection = _perspective_matrix(60.0, framebuffer_width / framebuffer_height, 1.0, 100000.0) + view = orbit.view_matrix() + mvp = projection @ view + + glUniformMatrix4fv(mvp_uniform_location, 1, GL_TRUE, mvp) + glBindVertexArray(vao) + glDrawArrays(GL_POINTS, 0, num_points) + glBindVertexArray(0) + + glfw.swap_buffers(window) + + +def _main() -> None: + window = _create_glfw_window() + + print("Initializing Zivid application") + app = zivid.Application() + + print("Verifying that CUDA backend is available") + compute_device = app.compute_device() + if compute_device.backend != zivid.ComputeBackend.cuda: + raise RuntimeError("This sample requires CUDA backend") + print(f"Using GPU: {compute_device.model}") + + print("Loading the CUDA runtime library that the Zivid SDK was built against") + cuda_runtime = _load_cuda_runtime(compute_device) + + unorganized_point_cloud = _capture_unorganized_point_cloud(app) + num_points = unorganized_point_cloud.size + + print("Getting GPU device arrays for points and colors (data stays on GPU)") + # The device arrays are synchronized into cuda_stream at acquisition, so the + # device pointers used during the copy below are plain accessors. + cuda_stream = zivid.CUDAStreamPtr() # default stream + xyzw_array = unorganized_point_cloud.device_points_xyzw(cuda_stream) + rgba_array = unorganized_point_cloud.device_colors(cuda_stream, zivid.PixelFormat.RGBA) + print(f"Position (XYZW): {xyzw_array.shape[0]} points, {xyzw_array.strides_in_bytes[0]} bytes/point") + print(f"Color (RGBA): {rgba_array.shape[0]} points, {rgba_array.strides_in_bytes[0]} bytes/point") + + print("Creating OpenGL vertex array and buffers") + program, vao, position_vbo, color_vbo = _create_vao_and_vbos(num_points, xyzw_array, rgba_array) + mvp_uniform_location = glGetUniformLocation(program, "mvp") + + print("Registering OpenGL buffers with CUDA (so CUDA can write into them)") + position_cuda_resource = cuda_runtime.graphics_gl_register_buffer(position_vbo) + color_cuda_resource = cuda_runtime.graphics_gl_register_buffer(color_vbo) + + print( + "Copying point cloud GPU -> GPU into OpenGL buffers (this is the CUDA-OpenGL interop step; no CPU round-trip)" + ) + _copy_device_array_to_vbo(cuda_runtime, xyzw_array, position_cuda_resource) + _copy_device_array_to_vbo(cuda_runtime, rgba_array, color_cuda_resource) + + print("Synchronizing CUDA stream to ensure transfer is complete") + cuda_runtime.stream_synchronize() + print("Transfer complete") + + print("Setting up orbit camera") + centroid = unorganized_point_cloud.centroid() + if centroid is not None: + center = centroid.tolist() + else: + center = [0.0, 0.0, 1000.0] + + # Initial distance: back away from the centroid along the Z axis + distance = max(abs(center[2]) * 0.8, 500.0) + orbit = _OrbitCamera(center, distance) + + glfw.set_mouse_button_callback(window, orbit.on_mouse_button) + glfw.set_cursor_pos_callback(window, orbit.on_cursor_pos) + glfw.set_scroll_callback(window, orbit.on_scroll) + + print("Configuring OpenGL state") + glEnable(GL_DEPTH_TEST) + glEnable(GL_PROGRAM_POINT_SIZE) + glClearColor(0.05, 0.05, 0.05, 1.0) + glUseProgram(program) + + print("Rendering. Left-drag to orbit, scroll to zoom, ESC to exit.") + _render_loop(window, orbit, mvp_uniform_location, vao, num_points) + + # Cleanup + cuda_runtime.graphics_unregister_resource(position_cuda_resource) + cuda_runtime.graphics_unregister_resource(color_cuda_resource) + glDeleteBuffers(2, [position_vbo, color_vbo]) + glDeleteVertexArrays(1, [vao]) + glDeleteProgram(program) + glfw.terminate() + print("Done") + + +if __name__ == "__main__": + _main() diff --git a/source/applications/advanced/cuda/capture_and_segment_image_with_pytorch_on_cuda.py b/source/applications/advanced/cuda/capture_and_segment_image_with_pytorch_on_cuda.py new file mode 100644 index 00000000..8206d2d7 --- /dev/null +++ b/source/applications/advanced/cuda/capture_and_segment_image_with_pytorch_on_cuda.py @@ -0,0 +1,301 @@ +""" +Demonstrate zero-copy GPU interop between Zivid and PyTorch/CuPy by feeding a Zivid 2D image into a third-party +segmentation model without a CPU round-trip. + +DISCLAIMER: Zivid does not provide segmentation, and Zivid does not support the third-party libraries used in this +sample. The segmentation model (DeepLabV3 from torchvision) and the GPU interop libraries (PyTorch, CuPy) are external +dependencies that the user is responsible for installing, configuring, and maintaining. This sample exists solely to +demonstrate how to pass Zivid GPU data to an external consumer without copying it through CPU memory. + +Only the handoff is zero-copy: Zivid produces the image on the GPU as a DeviceArray, CuPy wraps that GPU memory as an +ndarray, and PyTorch consumes the CuPy array via DLPack -- no CPU round-trip. The model's preprocessing, inference, and +output (`predictions`, `predictions_resized`) allocate new GPU buffers as usual, and `predictions.cpu().numpy()` at the +end copies the result to CPU for visualization. In a real pipeline you would keep results on the GPU. See the related +samples `capture_and_process_image_with_cupy_on_cuda.py` and `capture_and_render_point_cloud_with_opengl_on_cuda.py` +for other ways to consume Zivid GPU data without a CPU round-trip. + +Requirements: +- CUDA-capable GPU +- PyTorch with CUDA support: pip install torch torchvision +- CuPy for zero-copy GPU interop: pip install cupy-cuda12x + +""" + +from typing import List, Tuple + +import cv2 +import numpy as np +import zivid + + +def _print_disclaimer(lines: List[str]) -> None: + width = max(len(line) for line in lines) + 4 + border = "#" * width + print() + print(border) + for line in lines: + print(f"# {line.ljust(width - 4)} #") + print(border) + print() + + +try: + import torch + from torchvision.models.segmentation import DeepLabV3_MobileNet_V3_Large_Weights, deeplabv3_mobilenet_v3_large +except ImportError: + print("⚠️ Failed to import PyTorch and torchvision. They are installed via `pip install torch torchvision`.") + raise + +try: + import cupy as cp +except ImportError: + print("⚠️ Failed to import CuPy. It is installed via `pip install cupy-cuda12x` (adjust for your CUDA version).") + raise + + +def _create_color_palette(num_classes: int) -> np.ndarray: + """Create a color palette for visualizing segmentation classes. + + Args: + num_classes: Number of segmentation classes. + + Returns: + Color palette as numpy array with shape (num_classes, 3). + + """ + palette = np.zeros((num_classes, 3), dtype=np.uint8) + for i in range(num_classes): + r = (i * 100) % 256 + g = (i * 150 + 50) % 256 + b = (i * 200 + 100) % 256 + palette[i] = [r, g, b] + return palette + + +def _initialize_pytorch_cuda() -> torch.device: + """Initialize PyTorch CUDA and return the device. + + Returns: + PyTorch CUDA device. + + Raises: + RuntimeError: If PyTorch CUDA is not available. + + """ + if not torch.cuda.is_available(): + raise RuntimeError("PyTorch CUDA is not available") + + torch_device = torch.device("cuda") + print(f"PyTorch CUDA device: {torch.cuda.get_device_name()}") + + # Force CUDA initialization by creating a small tensor + torch.zeros(1, device=torch_device) + + return torch_device + + +def _load_segmentation_model(torch_device: torch.device) -> torch.nn.Module: + """Load and prepare the DeepLabV3 segmentation model. + + Args: + torch_device: PyTorch device to load the model on. + + Returns: + Loaded segmentation model in eval mode. + + """ + print("Loading DeepLabV3 segmentation model") + weights = DeepLabV3_MobileNet_V3_Large_Weights.DEFAULT + model = deeplabv3_mobilenet_v3_large(weights=weights) + model = model.to(torch_device) + model.eval() + return model + + +def _run_segmentation( + model: torch.nn.Module, + torch_image: torch.Tensor, + torch_device: torch.device, + original_size: Tuple[int, int], +) -> torch.Tensor: + """Run segmentation inference on the input image. + + Args: + model: Segmentation model. + torch_image: Input image tensor in HWC format with values in [0, 1]. + torch_device: PyTorch device. + original_size: Original image size (height, width) for resizing output. + + Returns: + Segmentation mask tensor resized to original size. + + """ + # Rearrange to CHW format and normalize for the model + torch_image = torch_image.permute(2, 0, 1).clamp(0, 1) + + # Resize for model (DeepLabV3 works best with specific sizes) + model_size = (520, 520) + torch_image_resized = torch.nn.functional.interpolate( + torch_image.unsqueeze(0), size=model_size, mode="bilinear", align_corners=False + ) + + # Apply model preprocessing (normalization) + mean = torch.tensor([0.485, 0.456, 0.406], device=torch_device).view(1, 3, 1, 1) + std = torch.tensor([0.229, 0.224, 0.225], device=torch_device).view(1, 3, 1, 1) + torch_image_normalized = (torch_image_resized - mean) / std + + # Run inference + print("Running segmentation inference on GPU") + with torch.no_grad(): + output = model(torch_image_normalized)["out"] + + # Get class predictions and resize to original size + predictions = output.argmax(1).squeeze(0) + predictions_resized = ( + torch.nn.functional.interpolate( + predictions.float().unsqueeze(0).unsqueeze(0), + size=original_size, + mode="nearest", + ) + .squeeze() + .byte() + ) + + print(f"Segmentation output shape: {predictions_resized.shape}") + print(f"Unique classes detected: {torch.unique(predictions_resized).cpu().numpy()}") + + return predictions_resized + + +def _visualize_and_save(segmentation_mask: np.ndarray, original_rgb: np.ndarray, width: int, height: int) -> None: + """Visualize segmentation results and save to file. + + Args: + segmentation_mask: Segmentation mask as numpy array. + original_rgb: Original RGB image as numpy array. + width: Original image width. + height: Original image height. + + """ + original_bgr = cv2.cvtColor(original_rgb, cv2.COLOR_RGB2BGR) + + # Create colored segmentation visualization + num_classes = 21 # PASCAL VOC classes + palette = _create_color_palette(num_classes) + segmentation_colored = palette[segmentation_mask] + + # Blend original image with segmentation + blended = cv2.addWeighted(original_bgr, 0.5, segmentation_colored, 0.5, 0) + + # Resize for display + display_width = 800 + scale = display_width / width + display_size = (display_width, int(height * scale)) + + original_display = cv2.resize(original_bgr, display_size) + segmentation_display = cv2.resize(segmentation_colored, display_size) + blended_display = cv2.resize(blended, display_size) + + # Stack images and add labels + combined_display = np.hstack([original_display, segmentation_display, blended_display]) + font = cv2.FONT_HERSHEY_SIMPLEX + cv2.putText(combined_display, "Original", (10, 30), font, 1, (0, 255, 0), 2) + cv2.putText(combined_display, "Segmentation", (display_width + 10, 30), font, 1, (0, 255, 0), 2) + cv2.putText(combined_display, "Blended", (2 * display_width + 10, 30), font, 1, (0, 255, 0), 2) + + # Display and save + cv2.imshow("GPU Interop Demo (third-party segmentation model)", combined_display) + print("Press any key to close...") + cv2.waitKey(0) + cv2.destroyAllWindows() + + cv2.imwrite("gpu_segmentation_result.png", combined_display) + print("Saved result to gpu_segmentation_result.png") + + +def _main() -> None: + _print_disclaimer( + [ + "DISCLAIMER", + "", + "This sample demonstrates ZERO-COPY GPU INTEROP between Zivid and PyTorch.", + "The segmentation model is a THIRD-PARTY component NOT provided or supported by Zivid.", + "The model is NOT expected to produce MEANINGFUL segmentation results.", + "It is included ONLY to show how to pass GPU data to an external consumer.", + ] + ) + + torch_device = _initialize_pytorch_cuda() + + print("Initializing Zivid application (will reuse PyTorch's CUDA context)") + app = zivid.Application() + + print("Verifying that CUDA backend is available") + compute_device = app.compute_device() + if compute_device.backend != zivid.ComputeBackend.cuda: + raise RuntimeError("This sample requires CUDA backend") + print(f"Using GPU: {compute_device.model}") + + _print_disclaimer( + [ + "DISCLAIMER", + "", + "Loading THIRD-PARTY segmentation model (DeepLabV3 from torchvision).", + "This model is NOT part of the Zivid SDK and is used here ONLY to illustrate GPU interop.", + ] + ) + model = _load_segmentation_model(torch_device) + + print("Connecting to camera") + camera = app.connect_camera() + settings_2d = zivid.Settings2D(acquisitions=[zivid.Settings2D.Acquisition()]) + + print("Capturing 2D frame") + frame_2d = camera.capture_2d(settings_2d) + + print("Creating a PyTorch CUDA stream for GPU operations") + pytorch_stream = torch.cuda.Stream() + print(f"Using CUDA stream: {hex(pytorch_stream.cuda_stream)}") + + print("Getting GPU device buffer in float format (RGBAf32), synchronized into the PyTorch stream") + # The DeviceArray is synchronized into zivid_stream at acquisition (via events, + # non-blocking), so consuming it on the same stream below needs no extra synchronization. + zivid_stream = zivid.CUDAStreamPtr(pytorch_stream.cuda_stream) + device_array = frame_2d.image_device_array(zivid_stream, zivid.PixelFormat.RGBAF) + height = device_array.shape[0] + width = device_array.shape[1] + print(f"Image size: {width}x{height}") + + print("Wrapping the stream for CuPy operations") + cupy_stream = cp.cuda.ExternalStream(pytorch_stream.cuda_stream) + + print("Running all GPU operations on the shared stream") + with torch.cuda.stream(pytorch_stream), cupy_stream: + print("Wrapping device buffer as a CuPy array (this is the zero-copy step; Zivid still owns the memory)") + cupy_rgba = cp.asarray(device_array) + + print("Extracting RGB channels and transferring to PyTorch via DLPack (zero-copy)") + cupy_rgb = cp.ascontiguousarray(cupy_rgba[:, :, :3]) + torch_image = torch.from_dlpack(cupy_rgb) + print(f"PyTorch tensor shape: {torch_image.shape}, dtype: {torch_image.dtype}") + + _print_disclaimer( + [ + "DISCLAIMER", + "", + "The segmentation model is NOT expected to produce MEANINGFUL results.", + "It is a generic model NOT trained for industrial 3D camera images.", + "The output is included ONLY to demonstrate the GPU interop pipeline.", + ] + ) + predictions_resized = _run_segmentation(model, torch_image, torch_device, (height, width)) + + print("Moving result GPU -> CPU for visualization (in a real pipeline you would keep this on the GPU)") + segmentation_mask = predictions_resized.cpu().numpy() + original_rgb = (cupy_rgb.get() * 255).astype(np.uint8) + + _visualize_and_save(segmentation_mask, original_rgb, width, height) + + +if __name__ == "__main__": + _main() diff --git a/source/applications/advanced/mask_point_cloud.py b/source/applications/advanced/mask_point_cloud.py new file mode 100644 index 00000000..620e671e --- /dev/null +++ b/source/applications/advanced/mask_point_cloud.py @@ -0,0 +1,63 @@ +""" +Read point cloud data from a ZDF file, apply a binary mask, and visualize it. + +The ZDF file for this sample can be found under the main instructions for Zivid samples. + +""" + +import cv2 +import numpy as np +import zivid +from zividsamples.display import display_depthmap, display_pointcloud, display_rgb +from zividsamples.paths import get_sample_data_path + + +def _main() -> None: + # Application class must be initialized before using other Zivid classes. + app = zivid.Application() # noqa: F841 # pylint: disable=unused-variable + + data_file = get_sample_data_path() / "Zivid3D.zdf" + print(f"Reading ZDF frame from file: {data_file}") + + frame = zivid.Frame(data_file) + point_cloud = frame.point_cloud() + xyz = point_cloud.copy_data("xyz") + rgba = point_cloud.copy_data("rgba_srgb") + + display_rgb(rgba[:, :, 0:3], title="RGB image") + + display_depthmap(xyz) + display_pointcloud(point_cloud) + + pixels_to_display = 300 + print(f"Generating binary mask of central {pixels_to_display} x {pixels_to_display} pixels") + height = frame.point_cloud().height + width = frame.point_cloud().width + mask = np.ones((height, width), bool) + + h_min = (height - pixels_to_display) // 2 + h_max = (height + pixels_to_display) // 2 + w_min = (width - pixels_to_display) // 2 + w_max = (width + pixels_to_display) // 2 + mask[h_min:h_max, w_min:w_max] = 0 + + print("Masking point cloud") + point_cloud.mask(mask) + + xyz_masked = point_cloud.copy_data("xyz") + display_depthmap(xyz_masked) + display_pointcloud(point_cloud) + + opencv_mask = np.ones((height, width), dtype=np.uint8) + center_x = width // 2 + center_y = height // 2 + radius = pixels_to_display + cv2.circle(opencv_mask, (center_x, center_y), radius, 0, thickness=cv2.FILLED) + + point_cloud.mask(opencv_mask.astype(bool)) + + display_pointcloud(point_cloud) + + +if __name__ == "__main__": + _main() diff --git a/source/applications/advanced/roi/roi_box_via_aruco_marker.py b/source/applications/advanced/roi/roi_box_via_aruco_marker.py index 2aa2c95a..40607e17 100644 --- a/source/applications/advanced/roi/roi_box_via_aruco_marker.py +++ b/source/applications/advanced/roi/roi_box_via_aruco_marker.py @@ -1,7 +1,8 @@ """ Filter the point cloud based on a ROI box given relative to the ArUco marker on a Zivid Calibration Board. -The ZFC file for this sample can be downloaded from https://support.zivid.com/en/latest/api-reference/samples/sample-data.html. +The ZDF file for this sample can be found in Zivid Sample Data. +See the instructions in README.md to download the Zivid Sample Data. For more information on Region-Of-Interest (ROI) and how to use it, check out this tutorial: https://support.zivid.com/en/latest/camera/academy/applications/roi.html @@ -40,13 +41,13 @@ def _transform_points(points: List[np.ndarray], transform: np.ndarray) -> List[n def _main() -> None: app = zivid.Application() - file_camera = get_sample_data_path() / "BinWithCalibrationBoard.zfc" + file_camera = get_sample_data_path() / "BinWithCalibrationBoard.zdf" + loaded_frame_with_diagnostics = zivid.Frame(file_camera) print(f"Creating virtual camera using file: {file_camera}") - camera = app.create_file_camera(file_camera) + camera = app.create_file_camera(loaded_frame_with_diagnostics) - settings = zivid.Settings([zivid.Settings.Acquisition()]) - settings.color = zivid.Settings2D(acquisitions=[zivid.Settings2D.Acquisition()]) + settings = loaded_frame_with_diagnostics.settings original_frame = camera.capture_2d_3d(settings) point_cloud = original_frame.point_cloud() @@ -101,19 +102,28 @@ def _main() -> None: ) print("Setting the ROI") - settings.region_of_interest.box.enabled = True - settings.region_of_interest.box.point_o = roi_points_in_camera_frame[0] - settings.region_of_interest.box.point_a = roi_points_in_camera_frame[1] - settings.region_of_interest.box.point_b = roi_points_in_camera_frame[2] - settings.region_of_interest.box.extents = (-10, roi_box_height) + roi_settings = zivid.Settings.RegionOfInterest.Box( + enabled=True, + point_o=roi_points_in_camera_frame[0], + point_a=roi_points_in_camera_frame[1], + point_b=roi_points_in_camera_frame[2], + extents=(-10, roi_box_height), + ) - roi_point_cloud = camera.capture_2d_3d(settings).point_cloud() + roi_point_cloud = point_cloud.masked_by_region_of_interest(roi_settings) print("Displaying the ROI-filtered point cloud") display_pointcloud(roi_point_cloud) print("Displaying depth map of the ROI-filtered point cloud") display_depthmap(roi_point_cloud.copy_data("xyz")) + print("Adding the ROI box to the capture settings and capturing again") + settings.region_of_interest.box = roi_settings + + roi_frame_point_cloud = camera.capture_2d_3d(settings).point_cloud() + print("Displaying the ROI-filtered point cloud from the new capture") + display_pointcloud(roi_frame_point_cloud) + if __name__ == "__main__": _main() diff --git a/source/applications/advanced/roi/roi_box_via_checkerboard.py b/source/applications/advanced/roi/roi_box_via_checkerboard.py index cdb3256f..c791977c 100644 --- a/source/applications/advanced/roi/roi_box_via_checkerboard.py +++ b/source/applications/advanced/roi/roi_box_via_checkerboard.py @@ -1,7 +1,8 @@ """ Filter the point cloud based on a ROI box given relative to the Zivid Calibration Board. -The ZFC file for this sample can be downloaded from https://support.zivid.com/en/latest/api-reference/samples/sample-data.html. +The ZDF file for this sample can be found in Zivid Sample Data. +See the instructions in README.md to download the Zivid Sample Data. For more information on Region-Of-Interest (ROI) and how to use it, check out this tutorial: https://support.zivid.com/en/latest/camera/academy/applications/roi.html @@ -40,14 +41,13 @@ def _transform_points(points: List[np.ndarray], transform: np.ndarray) -> List[n def _main() -> None: app = zivid.Application() - file_camera = get_sample_data_path() / "BinWithCalibrationBoard.zfc" + file_camera = get_sample_data_path() / "BinWithCalibrationBoard.zdf" + loaded_frame_with_diagnostics = zivid.Frame(file_camera) print(f"Creating virtual camera using file: {file_camera}") - camera = app.create_file_camera(file_camera) + camera = app.create_file_camera(loaded_frame_with_diagnostics) - settings = zivid.Settings() - settings.acquisitions.append(zivid.Settings.Acquisition()) - settings.color = zivid.Settings2D(acquisitions=[zivid.Settings2D.Acquisition()]) + settings = loaded_frame_with_diagnostics.settings original_frame = camera.capture_2d_3d(settings) point_cloud = original_frame.point_cloud() @@ -93,19 +93,28 @@ def _main() -> None: ) print("Setting the ROI") - settings.region_of_interest.box.enabled = True - settings.region_of_interest.box.point_o = roi_points_in_camera_frame[0] - settings.region_of_interest.box.point_a = roi_points_in_camera_frame[1] - settings.region_of_interest.box.point_b = roi_points_in_camera_frame[2] - settings.region_of_interest.box.extents = (-10, roi_box_height) + roi_settings = zivid.Settings.RegionOfInterest.Box( + enabled=True, + point_o=roi_points_in_camera_frame[0], + point_a=roi_points_in_camera_frame[1], + point_b=roi_points_in_camera_frame[2], + extents=(-10, roi_box_height), + ) - roi_point_cloud = camera.capture_2d_3d(settings).point_cloud() + roi_point_cloud = point_cloud.masked_by_region_of_interest(roi_settings) print("Displaying the ROI-filtered point cloud") display_pointcloud(roi_point_cloud) print("Displaying depth map of the ROI-filtered point cloud") display_depthmap(roi_point_cloud.copy_data("xyz")) + print("Adding the ROI box to the capture settings and capturing again") + settings.region_of_interest.box = roi_settings + + roi_frame_point_cloud = camera.capture_2d_3d(settings).point_cloud() + print("Displaying the ROI-filtered point cloud from the new capture") + display_pointcloud(roi_frame_point_cloud) + if __name__ == "__main__": _main() diff --git a/source/applications/advanced/stitching/stitch_using_robot_mounted_camera.py b/source/applications/advanced/stitching/stitch_using_robot_mounted_camera.py index 7ba4cc51..de64138f 100644 --- a/source/applications/advanced/stitching/stitch_using_robot_mounted_camera.py +++ b/source/applications/advanced/stitching/stitch_using_robot_mounted_camera.py @@ -11,6 +11,11 @@ The big object does not fit within the camera's field of view, so the stitching is done to extend the field of view of the camera, and see the object in full. +Before stitching, each point cloud is transformed to the robot base frame and cropped using a +region-of-interest (ROI) box defined in that frame. Because the ROI is applied as a post-capture +re-processing step, a single workspace volume relative to the robot base filters out background +from every viewpoint. + The resulting stitched point cloud is voxel downsampled if the `--full-resolution` flag is not set. Dataset: https://support.zivid.com/en/latest/api-reference/samples/sample-data.html @@ -66,12 +71,15 @@ def _options() -> argparse.Namespace: return parser.parse_args() -def _stitch_point_clouds(directory: Path, full_resolution: bool) -> zivid.UnorganizedPointCloud: +def _stitch_point_clouds( + directory: Path, full_resolution: bool, workspace_roi_box: zivid.Settings.RegionOfInterest.Box +) -> zivid.UnorganizedPointCloud: """Stitch multiple point clouds captured at different robot poses. Args: directory (Path): Path to directory containing point clouds in ZDF and robot poses and a hand-eye transform in YML format. full_resolution (bool): If True, use full resolution (no downsampling). Otherwise, voxel downsample. + workspace_roi_box (zivid.Settings.RegionOfInterest.Box): A region of interest box in robot base frame Returns: zivid.UnorganizedPointCloud: The stitched point cloud. @@ -104,11 +112,11 @@ def _stitch_point_clouds(directory: Path, full_resolution: bool) -> zivid.Unorga frame = zivid.Frame(zdf) base_to_camera_transform = np.matmul(robot_pose, hand_eye_transform) - unorganized_point_cloud_in_base_frame = ( - frame.point_cloud() - .to_unorganized_point_cloud() - .voxel_downsampled(voxel_size=1.0, min_points_per_voxel=2) - .transformed(base_to_camera_transform) + point_cloud = frame.point_cloud() + point_cloud.transform(base_to_camera_transform) + point_cloud.mask_by_region_of_interest(workspace_roi_box) + unorganized_point_cloud_in_base_frame = point_cloud.to_unorganized_point_cloud().voxel_downsampled( + voxel_size=1.0, min_points_per_voxel=2 ) if index != 0: @@ -144,9 +152,11 @@ def _stitch_point_clouds(directory: Path, full_resolution: bool) -> zivid.Unorga if full_resolution: for index, transform in enumerate(list_of_transforms): frame = zivid.Frame(zdf_files[index]) - frame.point_cloud().transform(transform[0]) + point_cloud = frame.point_cloud() + point_cloud.transform(transform[0]) + point_cloud.mask_by_region_of_interest(workspace_roi_box) final_point_cloud.transform(np.linalg.inv(transform[1])) - final_point_cloud.extend(frame.point_cloud().to_unorganized_point_cloud()) + final_point_cloud.extend(point_cloud.to_unorganized_point_cloud()) if index > 0: print(f"{index + 1} out of {len(list_of_transforms)} point clouds stitched.") @@ -185,13 +195,27 @@ def _main() -> None: ) print("Stitching small object...") - final_point_cloud_small = _stitch_point_clouds(small_object_dir, args.full_resolution) + small_workspace_roi_box = zivid.Settings.RegionOfInterest.Box( + enabled=True, + point_o=[-150, 300, 50], + point_a=[-150, 600, 50], + point_b=[150, 300, 50], + extents=(-75, 40), + ) + final_point_cloud_small = _stitch_point_clouds(small_object_dir, args.full_resolution, small_workspace_roi_box) display_pointcloud(final_point_cloud_small) file_name_small = Path(__file__).parent / "StitchedPointCloudSmallObject.ply" export_unorganized_point_cloud(final_point_cloud_small, PLY(str(file_name_small), layout=PLY.Layout.unordered)) print("Stitching big object...") - final_point_cloud_big = _stitch_point_clouds(big_object_dir, args.full_resolution) + big_workspace_roi_box = zivid.Settings.RegionOfInterest.Box( + enabled=True, + point_o=[-1000, 50, 50], + point_a=[-1000, 450, 50], + point_b=[750, 350, 50], + extents=(-150, 20), + ) + final_point_cloud_big = _stitch_point_clouds(big_object_dir, args.full_resolution, big_workspace_roi_box) display_pointcloud(final_point_cloud_big) file_name_big = Path(__file__).parent / "StitchedPointCloudBigObject.ply" export_unorganized_point_cloud(final_point_cloud_big, PLY(str(file_name_big), layout=PLY.Layout.unordered)) diff --git a/source/applications/basic/visualization/capture_from_file_camera_vis_3d.py b/source/applications/basic/visualization/capture_from_file_camera_vis_3d.py index 50c66c9d..e6adfd79 100644 --- a/source/applications/basic/visualization/capture_from_file_camera_vis_3d.py +++ b/source/applications/basic/visualization/capture_from_file_camera_vis_3d.py @@ -2,10 +2,11 @@ Capture point clouds, with color, with the Zivid file camera. This sample can be used without access to a physical camera. -The file camera files are found in Zivid Sample Data with ZFC file extension. +The file camera is created from a ZDF with diagnostics. +ZDF files with diagnostics are found in Zivid Sample Data. See the instructions in README.md to download the Zivid Sample Data. -There are five available file cameras to choose from, one for each camera model. -The default file camera used in this sample is the Zivid 2 M70 file camera. +There are nine available file cameras to choose from, one for each camera model. +The default ZDF used in this sample is from Zivid 2 M70. For more information about file cameras, check out this tutorial: https://support.zivid.com/en/latest/camera/academy/camera/file-camera.html @@ -34,8 +35,8 @@ def _options() -> argparse.Namespace: "--file-camera", required=False, type=Path, - default=get_sample_data_path() / "FileCameraZivid2M70.zfc", - help="Path to the file camera .zfc file", + default=get_sample_data_path() / "FileCameraZivid2M70.zdf", + help="Path to a ZDF with diagnostics enabled", ) return parser.parse_args() @@ -47,27 +48,26 @@ def _main() -> None: app = zivid.Application() file_camera = user_input.file_camera + loaded_frame_with_diagnostics = zivid.Frame(file_camera) print(f"Creating virtual camera using file: {file_camera}") - camera = app.create_file_camera(file_camera) + camera = app.create_file_camera(loaded_frame_with_diagnostics) - print("Configuring settings") - settings = zivid.Settings() - settings.acquisitions.append(zivid.Settings.Acquisition()) + print("Capturing frame") + settings = loaded_frame_with_diagnostics.settings settings.processing.filters.smoothing.gaussian.enabled = True - settings.processing.filters.smoothing.gaussian.sigma = 1 + settings.processing.filters.smoothing.gaussian.sigma = 1.5 settings.processing.filters.reflection.removal.enabled = True - settings.processing.filters.reflection.removal.mode = "global" - - settings_2d = zivid.Settings2D() - settings_2d.acquisitions.append(zivid.Settings2D.Acquisition()) - settings_2d.processing.color.balance.red = 1.0 - settings_2d.processing.color.balance.blue = 1.0 - settings_2d.processing.color.balance.green = 1.0 - - settings.color = settings_2d - - print("Capturing frame") + settings.processing.filters.reflection.removal.mode = ( + zivid.Settings.Processing.Filters.Reflection.Removal.Mode.global_ + ) + settings.region_of_interest.box = zivid.Settings.RegionOfInterest.Box( + enabled=True, + point_o=(-331, 201, 661), + point_a=(299, 203, 667), + point_b=(-331, -203, 844), + extents=(0, 178), + ) frame = camera.capture_2d_3d(settings) print("Visualizing point cloud") diff --git a/source/camera/basic/capture_from_file_camera.py b/source/camera/basic/capture_from_file_camera.py index 1b093996..8f2318f7 100644 --- a/source/camera/basic/capture_from_file_camera.py +++ b/source/camera/basic/capture_from_file_camera.py @@ -54,18 +54,21 @@ def _main() -> None: settings = zivid.Settings() settings.acquisitions.append(zivid.Settings.Acquisition()) settings.processing.filters.smoothing.gaussian.enabled = True - settings.processing.filters.smoothing.gaussian.sigma = 1 + settings.processing.filters.smoothing.gaussian.sigma = 1.5 settings.processing.filters.reflection.removal.enabled = True - settings.processing.filters.reflection.removal.mode = "global" - + settings.processing.filters.reflection.removal.mode = ( + zivid.Settings.Processing.Filters.Reflection.Removal.Mode.global_ + ) + settings.region_of_interest.box = zivid.Settings.RegionOfInterest.Box( + enabled=True, + point_o=(-266, 190, 771), + point_a=(203, 207, 771), + point_b=(-255, -131, 771), + extents=(0, 298), + ) settings_2d = zivid.Settings2D() settings_2d.acquisitions.append(zivid.Settings2D.Acquisition()) - settings_2d.processing.color.balance.blue = 1.0 - settings_2d.processing.color.balance.green = 1.0 - settings_2d.processing.color.balance.red = 1.0 - settings.color = settings_2d - print("Capturing frame") frame = camera.capture_2d_3d(settings) data_file = "Frame.zdf" diff --git a/source/camera/basic/capture_with_settings_from_yml.py b/source/camera/basic/capture_with_settings_from_yml.py index 9f85ee9b..3e2b50dc 100644 --- a/source/camera/basic/capture_with_settings_from_yml.py +++ b/source/camera/basic/capture_with_settings_from_yml.py @@ -3,6 +3,9 @@ Choose whether to get the image in the linear RGB or the sRGB color space. +Demonstrates saving and loading a 2D frame to and from a ZDF file, and using read_frame_file_type to determine +whether a ZDF file contains a 3D Frame or a Frame2D. + The YML files for this sample can be found under the main Zivid sample instructions. For more information about camera settings, check out this article: @@ -123,12 +126,26 @@ def _main() -> None: # More information about linear RGB and sRGB color spaces is available at: # https://support.zivid.com/en/latest/reference-articles/color-spaces-and-output-formats.html#color-spaces + data_file_2d = "Frame2D.zdf" + print(f"Saving 2D frame to file: {data_file_2d}") + frame_2d.save(data_file_2d) + + frame_file_type_2d = zivid.read_frame_file_type(data_file_2d) + print(f"Frame file type: {frame_file_type_2d}") + + print(f"Loading 2D frame from file: {data_file_2d}") + loaded_frame_2d = zivid.Frame2D(data_file_2d) + print(f"Loaded 2D frame: {loaded_frame_2d}") + print("Capturing 3D frame") frame_3d = camera.capture_3d(settings) data_file = "Frame3D.zdf" print(f"Saving frame to file: {data_file}") frame_3d.save(data_file) + frame_file_type = zivid.read_frame_file_type(data_file) + print(f"Frame file type: {frame_file_type}") + data_file_ply = "PointCloudWithoutColor.ply" print(f"Exporting point cloud (default pink colored points) to file: {data_file_ply}") frame_3d.save(data_file_ply) diff --git a/source/camera/basic/connect.py b/source/camera/basic/connect.py new file mode 100644 index 00000000..90ca3897 --- /dev/null +++ b/source/camera/basic/connect.py @@ -0,0 +1,58 @@ +""" +Connect to a Zivid camera using the different available methods. + +Replace the IP address and serial number in the code with the ones of your camera. + +""" + +import zivid + + +def _print_discovered_cameras(app: zivid.Application) -> None: + print("Discovered cameras:") + for camera in app.cameras(): + print(f"Serial number: {camera.info.serial_number}, IP address: {camera.state.network.ipv4.address}") + + +def _main() -> None: + app = zivid.Application() + + _print_discovered_cameras(app) + + print( + "The serial number, IP address and hostname below are placeholders. Replace them with the ones of your camera." + ) + + print("Connecting to the first available camera") + camera = app.connect_camera() + camera.disconnect() + + print("Connecting to the camera with a specific serial number") + camera = app.connect_camera(serial_number="2020C0DE") + camera.disconnect() + + print("Connecting to the camera at a specific IP address") + camera = app.connect_camera(address=zivid.CameraAddress("172.28.60.5")) + camera.disconnect() + + print("Connecting to the camera at a specific hostname") + # The default hostname format is "zivid-.local". + # The hostname cannot be read or set through the SDK. + camera = app.connect_camera(address=zivid.CameraAddress("zivid-2020C0DE.local")) + camera.disconnect() + + print("Connecting to all available cameras") + connected_cameras = [] + for camera in app.cameras(): + if camera.state.status == zivid.CameraState.Status.available: + print(f"Connecting to camera: {camera.info.serial_number}") + camera.connect() + connected_cameras.append(camera) + else: + print(f"Camera {camera.info.serial_number} is not available. Camera status: {camera.state.status}") + for camera in connected_cameras: + camera.disconnect() + + +if __name__ == "__main__": + _main() diff --git a/source/camera/basic/create_file_camera_from_zdf_with_diagnostics.py b/source/camera/basic/create_file_camera_from_zdf_with_diagnostics.py new file mode 100644 index 00000000..4737b34b --- /dev/null +++ b/source/camera/basic/create_file_camera_from_zdf_with_diagnostics.py @@ -0,0 +1,74 @@ +""" +Capture a frame with diagnostics enabled and create a file camera from it. + +A file camera is a virtual camera that replays captures offline using the raw sensor data +stored in the original frame. This allows you to develop and test without a physical camera. + +The workflow: +1. Capture a frame with diagnostics enabled +2. Save the diagnostics frame as a .zdf file +3. Disconnect from the camera (no longer needed) +4. Load the .zdf frame and create a file camera from it +5. Adjust processing settings and capture from the file camera +6. Save the resulting frame + +For more information about file cameras, check out this tutorial: +https://support.zivid.com/en/latest/camera/academy/camera/file-camera.html + +""" + +import zivid + + +def _main() -> None: + app = zivid.Application() + + print("Connecting to camera") + camera = app.connect_camera() + + print("Creating default settings") + settings = zivid.Settings( + acquisitions=[zivid.Settings.Acquisition()], + color=zivid.Settings2D(acquisitions=[zivid.Settings2D.Acquisition()]), + ) + print("Enabling diagnostics") + settings.diagnostics.enabled = True + + print("Capturing frame with diagnostics") + frame_with_diagnostics = camera.capture_2d_3d(settings) + + frame_with_diagnostics_file = "FrameWithDiagnostics.zdf" + print(f"Saving diagnostics frame to: {frame_with_diagnostics_file}") + frame_with_diagnostics.save(frame_with_diagnostics_file) + + print("Disconnecting from camera") + camera.disconnect() + + print(f"Loading ZDF with diagnostics enabled from: {frame_with_diagnostics_file}") + loaded_frame_with_diagnostics = zivid.Frame(frame_with_diagnostics_file) + + print("Creating file camera from frame") + file_camera = app.create_file_camera(loaded_frame_with_diagnostics) + + print(f"File camera info: {file_camera.info}") + + print("Configuring settings") + settings_from_frame = loaded_frame_with_diagnostics.settings + settings_from_frame.diagnostics.enabled = False + settings_from_frame.processing.filters.smoothing.gaussian.enabled = True + settings_from_frame.processing.filters.smoothing.gaussian.sigma = 1.5 + settings_from_frame.processing.filters.reflection.removal.enabled = True + settings_from_frame.processing.filters.reflection.removal.mode = ( + zivid.Settings.Processing.Filters.Reflection.Removal.Mode.global_ + ) + + print("Capturing from file camera") + file_camera_frame = file_camera.capture_2d_3d(settings_from_frame) + + file_camera_frame_file = "FrameFromFileCameraWithNoDiagnostics.zdf" + print(f"Saving file camera frame to: {file_camera_frame_file}") + file_camera_frame.save(file_camera_frame_file) + + +if __name__ == "__main__": + _main() diff --git a/source/camera/info_util_other/camera_info.py b/source/camera/info_util_other/camera_info.py index d17a0074..214e9466 100644 --- a/source/camera/info_util_other/camera_info.py +++ b/source/camera/info_util_other/camera_info.py @@ -15,8 +15,17 @@ def _main() -> None: print(f"Zivid SDK: {zivid.SDKVersion.full}") cameras = app.cameras() for camera in cameras: - print(f"Camera Info: {camera.info}") - print(f"Camera State: {camera.state}") + print(camera.info) + print(camera.state) + + for camera in cameras: + temperature = camera.state.temperature + print("Temperatures:") + print(f" DMD: {temperature.dmd} °C") + print(f" LED: {temperature.led} °C") + print(f" Lens: {temperature.lens} °C") + print(f" PCB: {temperature.pcb} °C") + print(f" General: {temperature.general} °C") if __name__ == "__main__": diff --git a/source/camera/info_util_other/capture_with_diagnostics.py b/source/camera/info_util_other/capture_with_diagnostics.py index 5b137383..48548156 100644 --- a/source/camera/info_util_other/capture_with_diagnostics.py +++ b/source/camera/info_util_other/capture_with_diagnostics.py @@ -1,8 +1,9 @@ """ -Capture point clouds, with color, from the Zivid camera, with default settings and diagnostics enabled. +Capture a 2D+3D frame and a 2D frame from the Zivid camera with diagnostics enabled. Enabling diagnostics allows collecting additional data to be saved in the ZDF file. Send ZDF files with diagnostics enabled to the Zivid support team to allow more thorough troubleshooting. +The 2D frame ZDF (Frame2DWithDiagnostics.zdf) must be loaded using the Frame2D API. Have in mind that enabling diagnostics increases the capture time and the RAM usage. For more information on diagnostics, check out this article: @@ -19,7 +20,7 @@ def _main() -> None: print("Connecting to camera") camera = app.connect_camera() - print("Configuring settings") + print("Configuring settings for 2D+3D capture") settings = zivid.Settings( acquisitions=[zivid.Settings.Acquisition()], color=zivid.Settings2D(acquisitions=[zivid.Settings2D.Acquisition()]), @@ -28,12 +29,24 @@ def _main() -> None: print("Enabling diagnostics") settings.diagnostics.enabled = True - print("Capturing frame") + print("Capturing 2D+3D frame") frame = camera.capture_2d_3d(settings) data_file = "FrameWithDiagnostics.zdf" print(f"Saving frame with diagnostic data to file: {data_file}") frame.save(data_file) + print("Configuring settings for 2D capture") + settings_2d = zivid.Settings2D(acquisitions=[zivid.Settings2D.Acquisition()]) + + print("Enabling 2D diagnostics") + settings_2d.diagnostics.enabled = True + + print("Capturing 2D frame") + frame_2d = camera.capture_2d(settings_2d) + data_file_2d = "Frame2DWithDiagnostics.zdf" + print(f"Saving 2D frame with diagnostic data to file: {data_file_2d}") + frame_2d.save(data_file_2d) + if __name__ == "__main__": _main() diff --git a/source/camera/info_util_other/check_health.py b/source/camera/info_util_other/check_health.py new file mode 100644 index 00000000..0b3fef49 --- /dev/null +++ b/source/camera/info_util_other/check_health.py @@ -0,0 +1,57 @@ +""" +Poll the camera health check from a separate thread while capturing in the main thread, printing the statuses and values every second. + +""" + +import threading +import time + +import zivid + + +def _print_healthcheck(health: zivid.CameraHealth) -> None: + temperature = health.temperature + print(f"Overall: {health.overall}") + print(f" Max transfer speed: {health.max_transfer_speed.status} ({health.max_transfer_speed.value} Mbps)") + print(f" Temperature (DMD): {temperature.dmd.status} ({temperature.dmd.value} C)") + print(f" Temperature (LED): {temperature.led.status} ({temperature.led.value} C)") + print(f" Temperature (Lens): {temperature.lens.status} ({temperature.lens.value} C)") + print(f" Fan: {health.fan.status} ({health.fan.value})") + print(f" Memory: {health.memory.status} ({health.memory.value} errors)") + print(f" Infield verification: {health.infield_verification.status} ({health.infield_verification.value})") + + +def _main() -> None: + app = zivid.Application() + + print("Connecting to camera") + camera = app.connect_camera() + + poll_interval = 1 + stop = threading.Event() + + def poll() -> None: + while not stop.is_set(): + _print_healthcheck(camera.check_health()) + print() + stop.wait(poll_interval) + + polling_thread = threading.Thread(target=poll) + polling_thread.start() + + settings = zivid.Settings(acquisitions=[zivid.Settings.Acquisition()]) + capture_cycle = 5 + number_of_captures = 5 + + for i in range(1, number_of_captures + 1): + camera.capture_3d(settings) + print(f"Captured frame {i} of {number_of_captures}") + if i < number_of_captures: + time.sleep(capture_cycle) + + stop.set() + polling_thread.join() + + +if __name__ == "__main__": + _main() diff --git a/source/camera/info_util_other/network/automatic_network_configuration_for_cameras.py b/source/camera/info_util_other/network/automatic_network_configuration_for_cameras.py index e76e06e8..27cc8836 100644 --- a/source/camera/info_util_other/network/automatic_network_configuration_for_cameras.py +++ b/source/camera/info_util_other/network/automatic_network_configuration_for_cameras.py @@ -4,7 +4,8 @@ Usage: - By default, the program applies the new configuration directly to the cameras. - Use the [--display-only] argument to simulate the configuration and display the - proposed IP addresses without making actual changes. + proposed IP addresses without making actual changes. Note: camera-side Ethernet + link speed will also be skipped, as it requires connecting to the camera. For more information on network configuration, check out this tutorial: https://support.zivid.com/en/latest/camera/getting-started/software-installation/zivid-two-network-configuration.html @@ -18,6 +19,39 @@ import zivid +def get_users_local_interface(camera: zivid.Camera) -> zivid.CameraState.Network.LocalInterface: + local_interfaces = camera.state.network.local_interfaces + + if len(local_interfaces) == 0: + raise RuntimeError(f"No user local interface detected from the camera {camera.info.serial_number}") + + if len(local_interfaces) > 1: + raise RuntimeError( + f"More than one local interface detected from the camera {camera.info.serial_number}. " + "Please, reorganize your network." + ) + + return local_interfaces[0] + + +def print_host_side_ethernet_link_speed(camera: zivid.Camera) -> None: + # Reading the host-side (local interface) Ethernet link speed does not require a connection to the camera. + local_interface_link_speed = get_users_local_interface(camera).ethernet.link_speed + + print(f"Camera {camera.info.serial_number}: local interface Ethernet link speed={local_interface_link_speed}") + + +def print_camera_side_ethernet_link_speed(camera: zivid.Camera) -> None: + # Reading the camera-side Ethernet link speed requires being connected to the camera. + camera.connect() + try: + camera_link_speed = camera.state.network.ethernet.link_speed + finally: + camera.disconnect() + + print(f"Camera {camera.info.serial_number}: camera Ethernet link speed={camera_link_speed}") + + def get_users_local_interface_network_configuration(camera: zivid.Camera) -> Tuple[str, str]: """Get the IP address and subnet mask of the user's local network interface connected to the specified camera. @@ -34,26 +68,17 @@ def get_users_local_interface_network_configuration(camera: zivid.Camera) -> Tup A tuple containing the IP address and subnet mask. """ - local_interfaces = camera.state.network.local_interfaces - - if len(local_interfaces) == 0: - raise RuntimeError(f"No user local interface detected from the camera {camera.info.serial_number}") + local_interface = get_users_local_interface(camera) - if len(local_interfaces) > 1: - raise RuntimeError( - f"More than one local interface detected from the camera {camera.info.serial_number}. " - "Please, reorganize your network." - ) - - if len(local_interfaces[0].ipv4.subnets) == 0: + if len(local_interface.ipv4.subnets) == 0: raise RuntimeError(f"No valid subnets found for camera {camera.info.serial_number}") - if len(local_interfaces[0].ipv4.subnets) > 1: + if len(local_interface.ipv4.subnets) > 1: raise RuntimeError( f"More than one ip address found for the local interface from the camera {camera.info.serial_number}" ) - subnet = local_interfaces[0].ipv4.subnets[0] + subnet = local_interface.ipv4.subnets[0] return subnet.address, subnet.mask @@ -79,6 +104,7 @@ def _main() -> None: for camera in cameras: try: + local_interface_ip_address, local_interface_subnet_mask = ( get_users_local_interface_network_configuration(camera) ) @@ -125,6 +151,18 @@ def _main() -> None: except RuntimeError as ex: print(f"Error when configuring camera: {camera.info.serial_number}. Exception: {ex}") + for camera in cameras: + + try: + print_host_side_ethernet_link_speed(camera) + if not args.display_only: + print_camera_side_ethernet_link_speed(camera) + + except RuntimeError as ex: + print( + f"Error when reading Ethernet link speed for camera: {camera.info.serial_number}. Exception: {ex}" + ) + except RuntimeError as ex: print(ex)