88
99import copy
1010from pathlib import Path
11- from typing import Dict , List , Optional
11+ from typing import Dict , List , Optional , Tuple
1212
1313import numpy as np
1414import zivid
1515from nptyping import NDArray , Shape , UInt8
1616from PyQt5 .QtCore import QSignalBlocker , pyqtSignal
1717from PyQt5 .QtWidgets import QHBoxLayout , QMessageBox , QPushButton , QVBoxLayout , QWidget
1818from zivid .experimental .hand_eye_low_dof import calibrate_eye_in_hand_low_dof , calibrate_eye_to_hand_low_dof
19- from zividsamples .gui .buttons_widget import HandEyeCalibrationButtonsWidget
20- from zividsamples .gui .cv2_handler import CV2Handler
21- from zividsamples .gui .detection_visualization import DetectionVisualizationWidget
22- from zividsamples .gui .hand_eye_configuration import CalibrationObject , HandEyeConfiguration
23- from zividsamples .gui .marker_widget import MarkerConfiguration
24- from zividsamples .gui .pose_pair_selection_widget import PosePair , PosePairSelectionWidget
25- from zividsamples .gui .pose_widget import PoseWidget , PoseWidgetDisplayMode
26- from zividsamples .gui .robot_configuration import RobotConfiguration
27- from zividsamples .gui .robot_control import RobotTarget
28- from zividsamples .gui .rotation_format_configuration import RotationInformation
29- from zividsamples .gui .set_fixed_objects import FixedCalibrationObjectsData , set_fixed_objects
30- from zividsamples .gui .settings_selector import SettingsPixelMappingIntrinsics
31- from zividsamples .gui .show_yaml_dialog import show_yaml_dialog
32- from zividsamples .gui .tab_with_robot_support import TabWidgetWithRobotSupport
33- from zividsamples .save_load_transformation_matrix import load_transformation_matrix , save_transformation_matrix
19+ from zividsamples .gui .calibration .calibration_buttons_widget import HandEyeCalibrationButtonsWidget
20+ from zividsamples .gui .calibration .pose_pair_selection_widget import (
21+ PosePair ,
22+ PosePairSelectionWidget ,
23+ SessionCalibrationConfig ,
24+ load_calibration_config ,
25+ save_calibration_config ,
26+ )
27+ from zividsamples .gui .calibration .set_fixed_objects import FixedCalibrationObjectsData , set_fixed_objects
28+ from zividsamples .gui .robot .robot_control import RobotTarget
29+ from zividsamples .gui .widgets .cv2_handler import CV2Handler
30+ from zividsamples .gui .widgets .detection_visualization import DetectionVisualizationWidget
31+ from zividsamples .gui .widgets .pose_widget import PoseWidget , PoseWidgetDisplayMode
32+ from zividsamples .gui .widgets .show_yaml_dialog import show_yaml_dialog
33+ from zividsamples .gui .widgets .tab_with_robot_support import TabWidgetWithRobotSupport
34+ from zividsamples .gui .wizard .hand_eye_configuration import CalibrationObject , HandEyeConfiguration
35+ from zividsamples .gui .wizard .marker_configuration import MarkerConfiguration
36+ from zividsamples .gui .wizard .robot_configuration import RobotConfiguration
37+ from zividsamples .gui .wizard .rotation_format_configuration import RotationInformation
38+ from zividsamples .gui .wizard .settings_selector import SettingsPixelMappingIntrinsics
39+ from zividsamples .save_load_transformation_matrix import save_transformation_matrix
3440from zividsamples .save_residuals import save_residuals
3541from zividsamples .transformation_matrix import TransformationMatrix
3642
@@ -45,6 +51,7 @@ class HandEyeCalibrationGUI(TabWidgetWithRobotSupport):
4551 checkerboard_pose_in_camera_frame : Optional [TransformationMatrix ] = None
4652 minimum_pose_pairs_for_calibration : int = 6
4753 calibration_finished = pyqtSignal (TransformationMatrix )
54+ loading_finished = pyqtSignal ()
4855 instructions_updated : pyqtSignal = pyqtSignal ()
4956 description : List [str ]
5057 fixed_objects : FixedCalibrationObjectsData
@@ -139,6 +146,13 @@ def connect_signals(self):
139146 self .robot_pose_widget .pose_updated .connect (self .on_robot_pose_manually_updated )
140147 self .pose_pair_selection_widget .pose_pair_clicked .connect (self .on_pose_pair_clicked )
141148 self .pose_pair_selection_widget .pose_pairs_updated .connect (self .on_pose_pairs_update )
149+ self .pose_pair_selection_widget .loading_finished .connect (self ._on_pose_pairs_loading_finished )
150+
151+ def _on_pose_pairs_loading_finished (self ) -> None :
152+ self .loading_finished .emit ()
153+ if self .pose_pair_selection_widget .number_of_active_pose_pairs () >= self .minimum_pose_pairs_for_calibration :
154+ save_to_disk = self .pose_pair_selection_widget .last_operation_was_reprocess
155+ self ._calibrate (save_to_disk = save_to_disk )
142156
143157 def update_instructions (self , has_detection_result : bool , robot_pose_confirmed : bool , calibrated : bool ):
144158 self .has_confirmed_robot_pose = robot_pose_confirmed
@@ -165,16 +179,68 @@ def update_instructions(self, has_detection_result: bool, robot_pose_confirmed:
165179 )
166180 self .confirm_robot_pose_button .setChecked (self .has_confirmed_robot_pose )
167181
182+ def _resolve_config_from_saved_session (
183+ self ,
184+ saved_config : SessionCalibrationConfig ,
185+ calibration_object : CalibrationObject ,
186+ marker_configuration : MarkerConfiguration ,
187+ ) -> Tuple [CalibrationObject , MarkerConfiguration ]:
188+ """If saved config differs from current, ask user and return (calibration_object, marker_configuration)."""
189+ mismatches = []
190+ if saved_config .calibration_object != calibration_object :
191+ mismatches .append (
192+ f"Calibration object: session={ saved_config .calibration_object .name } , "
193+ f"current={ calibration_object .name } "
194+ )
195+ if saved_config .eye_in_hand != self .hand_eye_configuration .eye_in_hand :
196+ saved_label = "Eye-In-Hand" if saved_config .eye_in_hand else "Eye-To-Hand"
197+ current_label = "Eye-In-Hand" if self .hand_eye_configuration .eye_in_hand else "Eye-To-Hand"
198+ mismatches .append (f"Configuration: session={ saved_label } , current={ current_label } " )
199+ if not mismatches :
200+ return (calibration_object , marker_configuration )
201+ reply = QMessageBox .question (
202+ self ,
203+ "Configuration Mismatch" ,
204+ "The saved session was captured with a different configuration:\n \n "
205+ + "\n " .join (f" - { m } " for m in mismatches )
206+ + "\n \n Load with saved configuration?" ,
207+ QMessageBox .Yes | QMessageBox .No ,
208+ )
209+ if reply != QMessageBox .Yes :
210+ return (calibration_object , marker_configuration )
211+ new_marker_config = marker_configuration
212+ if saved_config .marker_ids is not None and saved_config .marker_dictionary is not None :
213+ new_marker_config = MarkerConfiguration (
214+ id_list = saved_config .marker_ids , dictionary = saved_config .marker_dictionary
215+ )
216+ return (saved_config .calibration_object , new_marker_config )
217+
168218 def on_pending_changes (self ):
169- if self .data_directory_has_data ():
170- self .pose_pair_selection_widget .set_directory (self .data_directory )
171- self .pose_pair_selection_widget .load_pose_pairs (
172- calibration_object = self .hand_eye_configuration .calibration_object ,
173- marker_configuration = self .marker_configuration ,
219+ self .pose_pair_selection_widget .clear ()
220+ self .pose_pair_selection_widget .set_directory (self .data_directory )
221+ if not self .data_directory_has_data ():
222+ return
223+ calibration_object = self .hand_eye_configuration .calibration_object
224+ marker_configuration = self .marker_configuration
225+ if self .session_info is not None :
226+ saved_config = load_calibration_config (self .session_info )
227+ if saved_config is not None and self .is_current_tab ():
228+ calibration_object , marker_configuration = self ._resolve_config_from_saved_session (
229+ saved_config , calibration_object , marker_configuration
230+ )
231+ save_calibration_config (
232+ self .session_info ,
233+ calibration_object ,
234+ self .hand_eye_configuration .eye_in_hand ,
235+ marker_configuration ,
174236 )
175- self .calibration_finished .emit (load_transformation_matrix (self .data_directory / "hand_eye_transform.yaml" ))
176- else :
177- self .pose_pair_selection_widget .set_directory (self .data_directory )
237+ self .pose_pair_selection_widget .load_pose_pairs (
238+ calibration_object = calibration_object ,
239+ marker_configuration = marker_configuration ,
240+ )
241+
242+ def is_loading (self ) -> bool :
243+ return self .pose_pair_selection_widget .is_loading ()
178244
179245 def on_tab_visibility_changed (self , is_current : bool ):
180246 pass
@@ -187,10 +253,35 @@ def hand_eye_configuration_update(self, hand_eye_configuration: HandEyeConfigura
187253 4 if self .hand_eye_configuration .calibration_object == CalibrationObject .Checkerboard else 6
188254 )
189255 self .fixed_objects .update_hand_eye_configuration (self .hand_eye_configuration )
256+ self ._prompt_reprocess_if_needed ()
190257
191258 def marker_configuration_update (self , marker_configuration : MarkerConfiguration ):
192259 self .marker_configuration = marker_configuration
193260 self .fixed_objects .update_marker_configuration (self .marker_configuration )
261+ self ._prompt_reprocess_if_needed ()
262+
263+ def _prompt_reprocess_if_needed (self ) -> None :
264+ if self .pose_pair_selection_widget .number_of_active_pose_pairs () == 0 :
265+ return
266+ reply = QMessageBox .question (
267+ self ,
268+ "Reprocess Pose Pairs" ,
269+ "Do you want to reload data with the new configuration?\n \n "
270+ "Detection results will be recalculated from the captured frames." ,
271+ QMessageBox .Yes | QMessageBox .No ,
272+ )
273+ if reply == QMessageBox .Yes :
274+ self .pose_pair_selection_widget .reprocess_pose_pairs (
275+ self .hand_eye_configuration .calibration_object ,
276+ self .marker_configuration ,
277+ )
278+ if self .session_info is not None :
279+ save_calibration_config (
280+ self .session_info ,
281+ self .hand_eye_configuration .calibration_object ,
282+ self .hand_eye_configuration .eye_in_hand ,
283+ self .marker_configuration ,
284+ )
194285
195286 def rotation_format_update (self , rotation_information : RotationInformation ):
196287 self .robot_pose_widget .set_rotation_format (rotation_information )
@@ -289,6 +380,13 @@ def process_capture(self, frame: zivid.Frame, rgba: NDArray[Shape["N, M, 4"], UI
289380
290381 def use_data (self ):
291382 self .pose_pair_selection_widget .add_pose_pair (self .pose_pair )
383+ if self .session_info is not None :
384+ save_calibration_config (
385+ self .session_info ,
386+ self .hand_eye_configuration .calibration_object ,
387+ self .hand_eye_configuration .eye_in_hand ,
388+ self .marker_configuration ,
389+ )
292390 self .update_instructions (
293391 has_detection_result = False ,
294392 robot_pose_confirmed = False ,
@@ -305,6 +403,9 @@ def on_use_fixed_objects_toggled(self, checked: bool):
305403 self .fixed_objects = updated_fixed_objects
306404
307405 def on_calibrate_button_clicked (self ):
406+ self ._calibrate (save_to_disk = True )
407+
408+ def _calibrate (self , save_to_disk : bool = True ) -> None :
308409 try :
309410 detection_results = self .pose_pair_selection_widget .get_detection_results ()
310411 calibration_result = (
@@ -324,14 +425,17 @@ def on_calibrate_button_clicked(self):
324425 print ("Hand-Eye calibration OK" )
325426 print (f"Result:\n { calibration_result } " )
326427 hand_eye_transformation_matrix = TransformationMatrix .from_matrix (calibration_result .transform ())
327- hand_eye_transform_path = self .data_directory / "hand_eye_transform.yaml"
328- save_transformation_matrix (hand_eye_transformation_matrix , hand_eye_transform_path )
329428
330- hand_eye_residuals_path = self .data_directory / "hand_eye_residuals.yaml"
331- save_residuals (calibration_result .residuals (), hand_eye_residuals_path )
429+ if save_to_disk :
430+ hand_eye_transform_path = self .data_directory / "hand_eye_transform.yaml"
431+ save_transformation_matrix (hand_eye_transformation_matrix , hand_eye_transform_path )
432+ hand_eye_residuals_path = self .data_directory / "hand_eye_residuals.yaml"
433+ save_residuals (calibration_result .residuals (), hand_eye_residuals_path )
332434
333435 self .pose_pair_selection_widget .set_residuals (calibration_result .residuals ())
334- show_yaml_dialog (hand_eye_transform_path , "Hand Eye Calibration Transform" )
436+ if save_to_disk :
437+ hand_eye_transform_path = self .data_directory / "hand_eye_transform.yaml"
438+ show_yaml_dialog (hand_eye_transform_path , "Hand Eye Calibration Transform" )
335439 self .update_instructions (
336440 has_detection_result = False ,
337441 robot_pose_confirmed = False ,
0 commit comments