Skip to content

Commit 9f456db

Browse files
csu-bot-zividblauer4
authored andcommitted
Sample: refactor HEGUI modules
This is a major refactoring of the Hand Eye GUI. - Improve loading and saving data. It now happens in parallel in background processes. - Logs the choice of Hand Eye Configuration with the data, so you don't have to infer, remember, or guess. - A loaded session cannot be modified anymore (can't add data to it). - Reserve some more space for the residuals so they're not hidden (on many screens). - Standardize the settings choice flow for Hand-Eye GUI.
1 parent 06d3ac3 commit 9f456db

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+2010
-1226
lines changed

modules/zividsamples/gui/calibration/__init__.py

Whitespace-only changes.
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
from typing import List
2+
3+
from PyQt5.QtCore import pyqtSignal
4+
from PyQt5.QtWidgets import QApplication, QCheckBox, QGroupBox, QHBoxLayout, QPushButton, QWidget
5+
6+
7+
class HandEyeCalibrationButtonsWidget(QWidget):
8+
calibrate_button_clicked = pyqtSignal()
9+
use_fixed_objects_toggled = pyqtSignal(bool)
10+
11+
def __init__(self, parent=None):
12+
super().__init__(parent)
13+
14+
# Define buttons
15+
self.calibrate_button = QPushButton("Calibrate")
16+
self.calibrate_button.setObjectName("HandEye-calibrate_button")
17+
self.use_fixed_objects_checkbox = QCheckBox("Fixed Objects - for low DOF systems")
18+
self.use_fixed_objects_checkbox.setObjectName("HandEye-fixed_objects_checkbox")
19+
20+
# Connect signals
21+
self.calibrate_button.clicked.connect(self.on_calibrate_button_clicked)
22+
self.use_fixed_objects_checkbox.toggled.connect(self.on_use_fixed_objects_toggled)
23+
24+
# Add buttons to layout
25+
calibrate_group_box = QGroupBox("Calibrate")
26+
calibrate_group_box_layout = QHBoxLayout()
27+
calibrate_group_box.setLayout(calibrate_group_box_layout)
28+
29+
calibrate_group_box_layout.addWidget(self.calibrate_button)
30+
calibrate_group_box_layout.addWidget(self.use_fixed_objects_checkbox)
31+
32+
buttons_layout = QHBoxLayout()
33+
buttons_layout.addWidget(calibrate_group_box)
34+
35+
self.setLayout(buttons_layout)
36+
37+
def on_calibrate_button_clicked(self):
38+
self.calibrate_button.setStyleSheet("background-color: yellow;")
39+
QApplication.processEvents()
40+
self.calibrate_button_clicked.emit()
41+
self.calibrate_button.setStyleSheet("")
42+
43+
def on_use_fixed_objects_toggled(self, checked: bool):
44+
self.use_fixed_objects_toggled.emit(checked)
45+
46+
def disable_buttons(self):
47+
self.calibrate_button.setEnabled(False)
48+
49+
def enable_buttons(self):
50+
self.calibrate_button.setEnabled(True)
51+
52+
def get_tab_widgets_in_order(self) -> List[QWidget]:
53+
return [self.calibrate_button]

modules/zividsamples/gui/hand_eye_calibration_gui.py renamed to modules/zividsamples/gui/calibration/hand_eye_calibration_gui.py

Lines changed: 133 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -8,29 +8,35 @@
88

99
import copy
1010
from pathlib import Path
11-
from typing import Dict, List, Optional
11+
from typing import Dict, List, Optional, Tuple
1212

1313
import numpy as np
1414
import zivid
1515
from nptyping import NDArray, Shape, UInt8
1616
from PyQt5.QtCore import QSignalBlocker, pyqtSignal
1717
from PyQt5.QtWidgets import QHBoxLayout, QMessageBox, QPushButton, QVBoxLayout, QWidget
1818
from 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
3440
from zividsamples.save_residuals import save_residuals
3541
from 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\nLoad 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,

modules/zividsamples/gui/hand_eye_settings_tester.py renamed to modules/zividsamples/gui/calibration/hand_eye_settings_tester.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,15 @@
2727
from zivid.calibration import DetectionResult, DetectionResultFiducialMarkers, MarkerShape
2828
from zivid.experimental import PixelMapping
2929
from zividsamples.display import display_pointcloud
30-
from zividsamples.gui.buttons_widget import CameraButtonsWidget
31-
from zividsamples.gui.camera_selection import select_camera
32-
from zividsamples.gui.cv2_handler import CV2Handler
33-
from zividsamples.gui.hand_eye_configuration import CalibrationObject, HandEyeButtonsWidget, HandEyeConfiguration
34-
from zividsamples.gui.image_viewer import ImageViewer, ImageViewerDialog
35-
from zividsamples.gui.live_2d_widget import Live2DWidget
36-
from zividsamples.gui.marker_widget import MarkersWidget
3730
from zividsamples.gui.qt_application import ZividQtApplication
38-
from zividsamples.gui.settings_selector import SettingsForHandEyeGUI, select_settings_for_hand_eye
31+
from zividsamples.gui.widgets.camera_buttons_widget import CameraButtonsWidget
32+
from zividsamples.gui.widgets.cv2_handler import CV2Handler
33+
from zividsamples.gui.widgets.image_viewer import ImageViewer, ImageViewerDialog
34+
from zividsamples.gui.widgets.live_2d_widget import Live2DWidget
35+
from zividsamples.gui.wizard.camera_selection import select_camera
36+
from zividsamples.gui.wizard.hand_eye_configuration import CalibrationObject, HandEyeButtonsWidget, HandEyeConfiguration
37+
from zividsamples.gui.wizard.marker_configuration import MarkersWidget
38+
from zividsamples.gui.wizard.settings_selector import SettingsForHandEyeGUI, select_settings_for_hand_eye
3939
from zividsamples.transformation_matrix import TransformationMatrix
4040

4141

0 commit comments

Comments
 (0)