Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file.
53 changes: 53 additions & 0 deletions modules/zividsamples/gui/calibration/calibration_buttons_widget.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
from typing import List

from PyQt5.QtCore import pyqtSignal
from PyQt5.QtWidgets import QApplication, QCheckBox, QGroupBox, QHBoxLayout, QPushButton, QWidget


class HandEyeCalibrationButtonsWidget(QWidget):
calibrate_button_clicked = pyqtSignal()
use_fixed_objects_toggled = pyqtSignal(bool)

def __init__(self, parent=None):
super().__init__(parent)

# Define buttons
self.calibrate_button = QPushButton("Calibrate")
self.calibrate_button.setObjectName("HandEye-calibrate_button")
self.use_fixed_objects_checkbox = QCheckBox("Fixed Objects - for low DOF systems")
self.use_fixed_objects_checkbox.setObjectName("HandEye-fixed_objects_checkbox")

# Connect signals
self.calibrate_button.clicked.connect(self.on_calibrate_button_clicked)
self.use_fixed_objects_checkbox.toggled.connect(self.on_use_fixed_objects_toggled)

# Add buttons to layout
calibrate_group_box = QGroupBox("Calibrate")
calibrate_group_box_layout = QHBoxLayout()
calibrate_group_box.setLayout(calibrate_group_box_layout)

calibrate_group_box_layout.addWidget(self.calibrate_button)
calibrate_group_box_layout.addWidget(self.use_fixed_objects_checkbox)

buttons_layout = QHBoxLayout()
buttons_layout.addWidget(calibrate_group_box)

self.setLayout(buttons_layout)

def on_calibrate_button_clicked(self):
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):
self.use_fixed_objects_toggled.emit(checked)

def disable_buttons(self):
self.calibrate_button.setEnabled(False)

def enable_buttons(self):
self.calibrate_button.setEnabled(True)

def get_tab_widgets_in_order(self) -> List[QWidget]:
return [self.calibrate_button]
Original file line number Diff line number Diff line change
Expand Up @@ -8,29 +8,35 @@

import copy
from pathlib import Path
from typing import Dict, List, Optional
from typing import Dict, List, Optional, Tuple

import numpy as np
import zivid
from nptyping import NDArray, Shape, UInt8
from PyQt5.QtCore import QSignalBlocker, pyqtSignal
from PyQt5.QtWidgets import QHBoxLayout, QMessageBox, QPushButton, QVBoxLayout, QWidget
from zivid.experimental.hand_eye_low_dof import calibrate_eye_in_hand_low_dof, calibrate_eye_to_hand_low_dof
from zividsamples.gui.buttons_widget import HandEyeCalibrationButtonsWidget
from zividsamples.gui.cv2_handler import CV2Handler
from zividsamples.gui.detection_visualization import DetectionVisualizationWidget
from zividsamples.gui.hand_eye_configuration import CalibrationObject, HandEyeConfiguration
from zividsamples.gui.marker_widget import MarkerConfiguration
from zividsamples.gui.pose_pair_selection_widget import PosePair, PosePairSelectionWidget
from zividsamples.gui.pose_widget import PoseWidget, PoseWidgetDisplayMode
from zividsamples.gui.robot_configuration import RobotConfiguration
from zividsamples.gui.robot_control import RobotTarget
from zividsamples.gui.rotation_format_configuration import RotationInformation
from zividsamples.gui.set_fixed_objects import FixedCalibrationObjectsData, set_fixed_objects
from zividsamples.gui.settings_selector import SettingsPixelMappingIntrinsics
from zividsamples.gui.show_yaml_dialog import show_yaml_dialog
from zividsamples.gui.tab_with_robot_support import TabWidgetWithRobotSupport
from zividsamples.save_load_transformation_matrix import load_transformation_matrix, save_transformation_matrix
from zividsamples.gui.calibration.calibration_buttons_widget import HandEyeCalibrationButtonsWidget
from zividsamples.gui.calibration.pose_pair_selection_widget import (
PosePair,
PosePairSelectionWidget,
SessionCalibrationConfig,
load_calibration_config,
save_calibration_config,
)
from zividsamples.gui.calibration.set_fixed_objects import FixedCalibrationObjectsData, set_fixed_objects
from zividsamples.gui.robot.robot_control import RobotTarget
from zividsamples.gui.widgets.cv2_handler import CV2Handler
from zividsamples.gui.widgets.detection_visualization import DetectionVisualizationWidget
from zividsamples.gui.widgets.pose_widget import PoseWidget, PoseWidgetDisplayMode
from zividsamples.gui.widgets.show_yaml_dialog import show_yaml_dialog
from zividsamples.gui.widgets.tab_with_robot_support import TabWidgetWithRobotSupport
from zividsamples.gui.wizard.hand_eye_configuration import CalibrationObject, HandEyeConfiguration
from zividsamples.gui.wizard.marker_configuration import MarkerConfiguration
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.save_load_transformation_matrix import save_transformation_matrix
from zividsamples.save_residuals import save_residuals
from zividsamples.transformation_matrix import TransformationMatrix

Expand All @@ -45,6 +51,7 @@ class HandEyeCalibrationGUI(TabWidgetWithRobotSupport):
checkerboard_pose_in_camera_frame: Optional[TransformationMatrix] = None
minimum_pose_pairs_for_calibration: int = 6
calibration_finished = pyqtSignal(TransformationMatrix)
loading_finished = pyqtSignal()
instructions_updated: pyqtSignal = pyqtSignal()
description: List[str]
fixed_objects: FixedCalibrationObjectsData
Expand Down Expand Up @@ -139,6 +146,13 @@ def connect_signals(self):
self.robot_pose_widget.pose_updated.connect(self.on_robot_pose_manually_updated)
self.pose_pair_selection_widget.pose_pair_clicked.connect(self.on_pose_pair_clicked)
self.pose_pair_selection_widget.pose_pairs_updated.connect(self.on_pose_pairs_update)
self.pose_pair_selection_widget.loading_finished.connect(self._on_pose_pairs_loading_finished)

def _on_pose_pairs_loading_finished(self) -> None:
self.loading_finished.emit()
if self.pose_pair_selection_widget.number_of_active_pose_pairs() >= self.minimum_pose_pairs_for_calibration:
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):
self.has_confirmed_robot_pose = robot_pose_confirmed
Expand All @@ -165,16 +179,68 @@ def update_instructions(self, has_detection_result: bool, robot_pose_confirmed:
)
self.confirm_robot_pose_button.setChecked(self.has_confirmed_robot_pose)

def _resolve_config_from_saved_session(
self,
saved_config: SessionCalibrationConfig,
calibration_object: CalibrationObject,
marker_configuration: MarkerConfiguration,
) -> Tuple[CalibrationObject, MarkerConfiguration]:
"""If saved config differs from current, ask user and return (calibration_object, marker_configuration)."""
mismatches = []
if saved_config.calibration_object != calibration_object:
mismatches.append(
f"Calibration object: session={saved_config.calibration_object.name}, "
f"current={calibration_object.name}"
)
if saved_config.eye_in_hand != self.hand_eye_configuration.eye_in_hand:
saved_label = "Eye-In-Hand" if saved_config.eye_in_hand else "Eye-To-Hand"
current_label = "Eye-In-Hand" if self.hand_eye_configuration.eye_in_hand else "Eye-To-Hand"
mismatches.append(f"Configuration: session={saved_label}, current={current_label}")
if not mismatches:
return (calibration_object, marker_configuration)
reply = QMessageBox.question(
self,
"Configuration Mismatch",
"The saved session was captured with a different configuration:\n\n"
+ "\n".join(f" - {m}" for m in mismatches)
+ "\n\nLoad with saved configuration?",
QMessageBox.Yes | QMessageBox.No,
)
if reply != QMessageBox.Yes:
return (calibration_object, marker_configuration)
new_marker_config = marker_configuration
if saved_config.marker_ids is not None and saved_config.marker_dictionary is not None:
new_marker_config = MarkerConfiguration(
id_list=saved_config.marker_ids, dictionary=saved_config.marker_dictionary
)
return (saved_config.calibration_object, new_marker_config)

def on_pending_changes(self):
if self.data_directory_has_data():
self.pose_pair_selection_widget.set_directory(self.data_directory)
self.pose_pair_selection_widget.load_pose_pairs(
calibration_object=self.hand_eye_configuration.calibration_object,
marker_configuration=self.marker_configuration,
self.pose_pair_selection_widget.clear()
self.pose_pair_selection_widget.set_directory(self.data_directory)
if not self.data_directory_has_data():
return
calibration_object = self.hand_eye_configuration.calibration_object
marker_configuration = self.marker_configuration
if self.session_info is not None:
saved_config = load_calibration_config(self.session_info)
if saved_config is not None and self.is_current_tab():
calibration_object, marker_configuration = self._resolve_config_from_saved_session(
saved_config, calibration_object, marker_configuration
)
save_calibration_config(
self.session_info,
calibration_object,
self.hand_eye_configuration.eye_in_hand,
marker_configuration,
)
self.calibration_finished.emit(load_transformation_matrix(self.data_directory / "hand_eye_transform.yaml"))
else:
self.pose_pair_selection_widget.set_directory(self.data_directory)
self.pose_pair_selection_widget.load_pose_pairs(
calibration_object=calibration_object,
marker_configuration=marker_configuration,
)

def is_loading(self) -> bool:
return self.pose_pair_selection_widget.is_loading()

def on_tab_visibility_changed(self, is_current: bool):
pass
Expand All @@ -187,10 +253,35 @@ def hand_eye_configuration_update(self, hand_eye_configuration: HandEyeConfigura
4 if self.hand_eye_configuration.calibration_object == CalibrationObject.Checkerboard else 6
)
self.fixed_objects.update_hand_eye_configuration(self.hand_eye_configuration)
self._prompt_reprocess_if_needed()

def marker_configuration_update(self, marker_configuration: MarkerConfiguration):
self.marker_configuration = marker_configuration
self.fixed_objects.update_marker_configuration(self.marker_configuration)
self._prompt_reprocess_if_needed()

def _prompt_reprocess_if_needed(self) -> None:
if self.pose_pair_selection_widget.number_of_active_pose_pairs() == 0:
return
reply = QMessageBox.question(
self,
"Reprocess Pose Pairs",
"Do you want to reload data with the new configuration?\n\n"
"Detection results will be recalculated from the captured frames.",
QMessageBox.Yes | QMessageBox.No,
)
if reply == QMessageBox.Yes:
self.pose_pair_selection_widget.reprocess_pose_pairs(
self.hand_eye_configuration.calibration_object,
self.marker_configuration,
)
if self.session_info is not None:
save_calibration_config(
self.session_info,
self.hand_eye_configuration.calibration_object,
self.hand_eye_configuration.eye_in_hand,
self.marker_configuration,
)

def rotation_format_update(self, rotation_information: RotationInformation):
self.robot_pose_widget.set_rotation_format(rotation_information)
Expand Down Expand Up @@ -289,6 +380,13 @@ def process_capture(self, frame: zivid.Frame, rgba: NDArray[Shape["N, M, 4"], UI

def use_data(self):
self.pose_pair_selection_widget.add_pose_pair(self.pose_pair)
if self.session_info is not None:
save_calibration_config(
self.session_info,
self.hand_eye_configuration.calibration_object,
self.hand_eye_configuration.eye_in_hand,
self.marker_configuration,
)
self.update_instructions(
has_detection_result=False,
robot_pose_confirmed=False,
Expand All @@ -305,6 +403,9 @@ def on_use_fixed_objects_toggled(self, checked: bool):
self.fixed_objects = updated_fixed_objects

def on_calibrate_button_clicked(self):
self._calibrate(save_to_disk=True)

def _calibrate(self, save_to_disk: bool = True) -> None:
try:
detection_results = self.pose_pair_selection_widget.get_detection_results()
calibration_result = (
Expand All @@ -324,14 +425,17 @@ def on_calibrate_button_clicked(self):
print("Hand-Eye calibration OK")
print(f"Result:\n{calibration_result}")
hand_eye_transformation_matrix = TransformationMatrix.from_matrix(calibration_result.transform())
hand_eye_transform_path = self.data_directory / "hand_eye_transform.yaml"
save_transformation_matrix(hand_eye_transformation_matrix, hand_eye_transform_path)

hand_eye_residuals_path = self.data_directory / "hand_eye_residuals.yaml"
save_residuals(calibration_result.residuals(), hand_eye_residuals_path)
if save_to_disk:
hand_eye_transform_path = self.data_directory / "hand_eye_transform.yaml"
save_transformation_matrix(hand_eye_transformation_matrix, hand_eye_transform_path)
hand_eye_residuals_path = self.data_directory / "hand_eye_residuals.yaml"
save_residuals(calibration_result.residuals(), hand_eye_residuals_path)

self.pose_pair_selection_widget.set_residuals(calibration_result.residuals())
show_yaml_dialog(hand_eye_transform_path, "Hand Eye Calibration Transform")
if save_to_disk:
hand_eye_transform_path = self.data_directory / "hand_eye_transform.yaml"
show_yaml_dialog(hand_eye_transform_path, "Hand Eye Calibration Transform")
self.update_instructions(
has_detection_result=False,
robot_pose_confirmed=False,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,15 @@
from zivid.calibration import DetectionResult, DetectionResultFiducialMarkers, MarkerShape
from zivid.experimental import PixelMapping
from zividsamples.display import display_pointcloud
from zividsamples.gui.buttons_widget import CameraButtonsWidget
from zividsamples.gui.camera_selection import select_camera
from zividsamples.gui.cv2_handler import CV2Handler
from zividsamples.gui.hand_eye_configuration import CalibrationObject, HandEyeButtonsWidget, HandEyeConfiguration
from zividsamples.gui.image_viewer import ImageViewer, ImageViewerDialog
from zividsamples.gui.live_2d_widget import Live2DWidget
from zividsamples.gui.marker_widget import MarkersWidget
from zividsamples.gui.qt_application import ZividQtApplication
from zividsamples.gui.settings_selector import SettingsForHandEyeGUI, select_settings_for_hand_eye
from zividsamples.gui.widgets.camera_buttons_widget import CameraButtonsWidget
from zividsamples.gui.widgets.cv2_handler import CV2Handler
from zividsamples.gui.widgets.image_viewer import ImageViewer, ImageViewerDialog
from zividsamples.gui.widgets.live_2d_widget import Live2DWidget
from zividsamples.gui.wizard.camera_selection import select_camera
from zividsamples.gui.wizard.hand_eye_configuration import CalibrationObject, HandEyeButtonsWidget, HandEyeConfiguration
from zividsamples.gui.wizard.marker_configuration import MarkersWidget
from zividsamples.gui.wizard.settings_selector import SettingsForHandEyeGUI, select_settings_for_hand_eye
from zividsamples.transformation_matrix import TransformationMatrix


Expand Down
Loading
Loading