Skip to content

Commit 6cc4731

Browse files
authored
Merge pull request #212 from zivid/2025-09-03-update-python-samples
Samples: HandEyeGUI improvements and fixes
2 parents 9faffb3 + 9e72e38 commit 6cc4731

23 files changed

Lines changed: 1103 additions & 554 deletions

continuous-integration/setup.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ apt-yes update || exit
1313
apt-yes dist-upgrade || exit
1414

1515
apt-yes install \
16+
git \
1617
python3-pip \
1718
wget ||
1819
exit $?

modules/pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ dependencies = [
2727
"pyqtgraph",
2828
"robodk",
2929
"scipy",
30-
"zivid"
30+
"zivid",
31+
"urrtde @ git+https://github.com/UniversalRobots/RTDE_Python_Client_Library.git@v2.7.12"
3132
]
3233

3334
[build-system]

modules/zividsamples/gui/buttons_widget.py

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -99,23 +99,19 @@ def get_tab_widgets_in_order(self) -> List[QWidget]:
9999

100100

101101
class HandEyeCalibrationButtonsWidget(QWidget):
102-
use_data_button_clicked = pyqtSignal()
103102
calibrate_button_clicked = pyqtSignal()
104103
use_fixed_objects_toggled = pyqtSignal(bool)
105104

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

109108
# Define buttons
110-
self.use_data_button = QPushButton("Use Data")
111-
self.use_data_button.setObjectName("HandEye-use_data_button")
112109
self.calibrate_button = QPushButton("Calibrate")
113110
self.calibrate_button.setObjectName("HandEye-calibrate_button")
114111
self.use_fixed_objects_checkbox = QCheckBox("Fixed Objects - for low DOF systems")
115112
self.use_fixed_objects_checkbox.setObjectName("HandEye-fixed_objects_checkbox")
116113

117114
# Connect signals
118-
self.use_data_button.clicked.connect(self.on_use_data_button_clicked)
119115
self.calibrate_button.clicked.connect(self.on_calibrate_button_clicked)
120116
self.use_fixed_objects_checkbox.toggled.connect(self.on_use_fixed_objects_toggled)
121117

@@ -124,7 +120,6 @@ def __init__(self, parent=None):
124120
calibrate_group_box_layout = QHBoxLayout()
125121
calibrate_group_box.setLayout(calibrate_group_box_layout)
126122

127-
calibrate_group_box_layout.addWidget(self.use_data_button)
128123
calibrate_group_box_layout.addWidget(self.calibrate_button)
129124
calibrate_group_box_layout.addWidget(self.use_fixed_objects_checkbox)
130125

@@ -139,25 +134,17 @@ def on_calibrate_button_clicked(self):
139134
self.calibrate_button_clicked.emit()
140135
self.calibrate_button.setStyleSheet("")
141136

142-
def on_use_data_button_clicked(self):
143-
self.use_data_button.setStyleSheet("background-color: yellow;")
144-
QApplication.processEvents()
145-
self.use_data_button_clicked.emit()
146-
self.use_data_button.setStyleSheet("")
147-
148137
def on_use_fixed_objects_toggled(self, checked: bool):
149138
self.use_fixed_objects_toggled.emit(checked)
150139

151140
def disable_buttons(self):
152-
self.use_data_button.setEnabled(False)
153141
self.calibrate_button.setEnabled(False)
154142

155143
def enable_buttons(self):
156-
self.use_data_button.setEnabled(True)
157144
self.calibrate_button.setEnabled(True)
158145

159146
def get_tab_widgets_in_order(self) -> List[QWidget]:
160-
return [self.use_data_button, self.calibrate_button]
147+
return [self.calibrate_button]
161148

162149

163150
class HandEyeVerificationButtonsWidget(QWidget):

modules/zividsamples/gui/hand_eye_calibration_gui.py

Lines changed: 25 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from zividsamples.gui.marker_widget import MarkerConfiguration
2424
from zividsamples.gui.pose_pair_selection_widget import PosePair, PosePairSelectionWidget, directory_has_pose_pair_data
2525
from zividsamples.gui.pose_widget import PoseWidget, PoseWidgetDisplayMode
26+
from zividsamples.gui.robot_configuration import RobotConfiguration
2627
from zividsamples.gui.robot_control import RobotTarget
2728
from zividsamples.gui.rotation_format_configuration import RotationInformation
2829
from zividsamples.gui.set_fixed_objects import FixedCalibrationObjectsData, set_fixed_objects
@@ -33,7 +34,7 @@
3334

3435
class HandEyeCalibrationGUI(QWidget):
3536
data_directory: Path
36-
use_robot: bool
37+
robot_configuration: RobotConfiguration
3738
hand_eye_configuration: HandEyeConfiguration
3839
marker_configuration: MarkerConfiguration = MarkerConfiguration()
3940
pose_pair: PosePair
@@ -51,7 +52,7 @@ class HandEyeCalibrationGUI(QWidget):
5152
def __init__(
5253
self,
5354
data_directory: Path,
54-
use_robot: bool,
55+
robot_configuration: RobotConfiguration,
5556
hand_eye_configuration: HandEyeConfiguration,
5657
marker_configuration: MarkerConfiguration,
5758
cv2_handler: CV2Handler,
@@ -69,7 +70,7 @@ def __init__(
6970
]
7071

7172
self.data_directory = data_directory
72-
self.use_robot = use_robot
73+
self.robot_configuration = robot_configuration
7374
self.hand_eye_configuration = hand_eye_configuration
7475
self.marker_configuration = marker_configuration
7576
self.fixed_objects = FixedCalibrationObjectsData(
@@ -84,7 +85,6 @@ def __init__(
8485
self.update_instructions(
8586
has_detection_result=self.has_detection_result,
8687
robot_pose_confirmed=self.has_confirmed_robot_pose,
87-
used_data=False,
8888
calibrated=False,
8989
)
9090

@@ -98,15 +98,14 @@ def create_widgets(self, initial_rotation_information: RotationInformation):
9898
self.robot_pose_widget.setObjectName("HE-Calibration-robot_pose_widget")
9999
self.confirm_robot_pose_button = QPushButton("Confirm Robot Pose")
100100
self.confirm_robot_pose_button.setCheckable(True)
101-
self.confirm_robot_pose_button.setVisible(not self.use_robot)
101+
self.confirm_robot_pose_button.setVisible(self.robot_configuration.has_no_robot())
102102
self.confirm_robot_pose_button.setObjectName("HE-Calibration-confirm_robot_pose_button")
103103
self.detection_visualization_widget = DetectionVisualizationWidget(
104104
hand_eye_configuration=self.hand_eye_configuration
105105
)
106106
self.pose_pair_selection_widget = PosePairSelectionWidget(directory=self.data_directory)
107107
self.pose_pair_selection_widget.setVisible(False)
108108
self.hand_eye_calibration_buttons = HandEyeCalibrationButtonsWidget()
109-
self.hand_eye_calibration_buttons.use_data_button.setEnabled(False)
110109
self.hand_eye_calibration_buttons.calibrate_button.setEnabled(False)
111110
self.hand_eye_calibration_buttons.setObjectName("HE-Calibration-hand_eye_calibration_buttons")
112111

@@ -134,24 +133,21 @@ def setup_layout(self):
134133
self.setLayout(layout)
135134

136135
def connect_signals(self):
137-
self.hand_eye_calibration_buttons.use_data_button_clicked.connect(self.on_use_data_button_clicked)
138136
self.hand_eye_calibration_buttons.calibrate_button_clicked.connect(self.on_calibrate_button_clicked)
139137
self.hand_eye_calibration_buttons.use_fixed_objects_toggled.connect(self.on_use_fixed_objects_toggled)
140138
self.confirm_robot_pose_button.clicked.connect(self.on_confirm_robot_pose_button_clicked)
141139
self.robot_pose_widget.pose_updated.connect(self.on_robot_pose_manually_updated)
142140
self.pose_pair_selection_widget.pose_pair_clicked.connect(self.on_pose_pair_clicked)
143141
self.pose_pair_selection_widget.pose_pairs_updated.connect(self.on_pose_pairs_update)
144142

145-
def update_instructions(
146-
self, has_detection_result: bool, robot_pose_confirmed: bool, used_data: bool, calibrated: bool
147-
):
143+
def update_instructions(self, has_detection_result: bool, robot_pose_confirmed: bool, calibrated: bool):
148144
self.has_confirmed_robot_pose = robot_pose_confirmed
149145
self.has_detection_result = has_detection_result and self.has_confirmed_robot_pose
150146
minimum_captures_to_go = (
151147
self.minimum_pose_pairs_for_calibration - self.pose_pair_selection_widget.number_of_active_pose_pairs()
152148
)
153149
self.instruction_steps = {}
154-
if self.use_robot:
150+
if self.robot_configuration.can_control:
155151
self.instruction_steps[
156152
"Move Robot (click 'Move to next target', 'Home' or Disconnect→manually move robot→Connect)"
157153
] = self.has_confirmed_robot_pose
@@ -161,13 +157,9 @@ def update_instructions(
161157
self.instruction_steps[f"Capture (at least {minimum_captures_to_go} more)"] = self.has_detection_result
162158
else:
163159
self.instruction_steps["Capture"] = self.has_detection_result
164-
self.instruction_steps["Use data"] = used_data
165160
if minimum_captures_to_go <= 0:
166161
self.instruction_steps["Calibrate"] = calibrated
167162
self.instructions_updated.emit()
168-
self.hand_eye_calibration_buttons.use_data_button.setEnabled(
169-
self.has_detection_result and self.has_confirmed_robot_pose
170-
)
171163
self.confirm_robot_pose_button.setStyleSheet(
172164
"background-color: green;" if self.has_confirmed_robot_pose else ""
173165
)
@@ -189,6 +181,15 @@ def marker_configuration_update(self, marker_configuration: MarkerConfiguration)
189181
def rotation_format_update(self, rotation_format: RotationInformation):
190182
self.robot_pose_widget.set_rotation_format(rotation_format)
191183

184+
def robot_configuration_update(self, robot_configuration: RobotConfiguration):
185+
self.robot_configuration = robot_configuration
186+
self.confirm_robot_pose_button.setVisible(self.robot_configuration.has_no_robot())
187+
self.update_instructions(
188+
has_detection_result=self.has_detection_result,
189+
robot_pose_confirmed=self.has_confirmed_robot_pose,
190+
calibrated=False,
191+
)
192+
192193
def on_select_fixed_objects_action_triggered(self):
193194
updated_fixed_objects = set_fixed_objects(self.fixed_objects, self.robot_pose_widget.rotation_information)
194195
if updated_fixed_objects is not None:
@@ -197,16 +198,6 @@ def on_select_fixed_objects_action_triggered(self):
197198
def toggle_advanced_view(self, checked):
198199
self.robot_pose_widget.toggle_advanced_section(checked)
199200

200-
def toggle_use_robot(self, use_robot: bool):
201-
self.use_robot = use_robot
202-
self.confirm_robot_pose_button.setVisible(not self.use_robot)
203-
self.update_instructions(
204-
has_detection_result=self.has_detection_result,
205-
robot_pose_confirmed=self.has_confirmed_robot_pose,
206-
used_data=False,
207-
calibrated=False,
208-
)
209-
210201
def on_start_auto_run(self) -> bool:
211202
if self.pose_pair_selection_widget.number_of_active_pose_pairs() == 0:
212203
return True
@@ -270,22 +261,23 @@ def process_capture(self, frame: zivid.Frame, rgba: NDArray[Shape["N, M, 4"], UI
270261
detection_result=detection_result,
271262
camera_pose=copy.deepcopy(camera_pose),
272263
)
273-
self.update_instructions(
274-
has_detection_result=True,
275-
robot_pose_confirmed=self.has_confirmed_robot_pose,
276-
used_data=False,
277-
calibrated=False,
278-
)
264+
if self.has_confirmed_robot_pose:
265+
self.use_data()
266+
else:
267+
self.update_instructions(
268+
has_detection_result=True,
269+
robot_pose_confirmed=self.has_confirmed_robot_pose,
270+
calibrated=False,
271+
)
279272
except RuntimeError as ex:
280273
self.detection_visualization_widget.set_error_message(str(ex))
281274
raise ex
282275

283-
def on_use_data_button_clicked(self):
276+
def use_data(self):
284277
self.pose_pair_selection_widget.add_pose_pair(self.pose_pair)
285278
self.update_instructions(
286279
has_detection_result=False,
287280
robot_pose_confirmed=False,
288-
used_data=True,
289281
calibrated=False,
290282
)
291283

@@ -325,7 +317,6 @@ def on_calibrate_button_clicked(self):
325317
self.update_instructions(
326318
has_detection_result=False,
327319
robot_pose_confirmed=False,
328-
used_data=False,
329320
calibrated=True,
330321
)
331322
self.calibration_finished.emit(TransformationMatrix.from_matrix(hand_eye_transform))
@@ -340,7 +331,6 @@ def confirm_robot_pose(self, confirmed: bool = True):
340331
self.update_instructions(
341332
has_detection_result=False,
342333
robot_pose_confirmed=confirmed,
343-
used_data=False,
344334
calibrated=False,
345335
)
346336

modules/zividsamples/gui/hand_eye_configuration.py

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
from dataclasses import dataclass
22
from enum import Enum
3+
from typing import Optional
34

4-
from PyQt5.QtCore import pyqtSignal
5+
from PyQt5.QtCore import QSettings, pyqtSignal
56
from PyQt5.QtWidgets import (
67
QButtonGroup,
8+
QCheckBox,
79
QDialog,
810
QDialogButtonBox,
911
QGroupBox,
@@ -22,8 +24,31 @@ class CalibrationObject(Enum):
2224

2325
@dataclass
2426
class HandEyeConfiguration:
25-
eye_in_hand: bool = True
26-
calibration_object: CalibrationObject = CalibrationObject.Checkerboard
27+
28+
def __init__(self, *, eye_in_hand: Optional[bool] = None, calibration_object: Optional[CalibrationObject] = None):
29+
settings = QSettings("Zivid", "HandEyeGUI")
30+
settings.beginGroup("hand_eye_configuration")
31+
if eye_in_hand is not None:
32+
self.eye_in_hand = eye_in_hand
33+
else:
34+
self.eye_in_hand = settings.value("eye_in_hand", True, type=bool)
35+
if calibration_object is not None:
36+
self.calibration_object = calibration_object
37+
else:
38+
calibration_object_name = settings.value(
39+
"calibration_object", CalibrationObject.Checkerboard.name, type=str
40+
)
41+
self.calibration_object = CalibrationObject[calibration_object_name]
42+
self.show_dialog = settings.value("show_dialog", True, type=bool)
43+
settings.endGroup()
44+
45+
def save_choice(self):
46+
settings = QSettings("Zivid", "HandEyeGUI")
47+
settings.beginGroup("hand_eye_configuration")
48+
settings.setValue("eye_in_hand", self.eye_in_hand)
49+
settings.setValue("calibration_object", self.calibration_object.name)
50+
settings.setValue("show_dialog", self.show_dialog)
51+
settings.endGroup()
2752

2853

2954
class HandEyeButtonsWidget(QWidget):
@@ -147,25 +172,34 @@ def __init__(
147172
self.button_box = QDialogButtonBox(QDialogButtonBox.Ok)
148173
self.button_box.accepted.connect(self.accept)
149174

175+
self.remember_choice_checkbox = QCheckBox("Show this dialog")
176+
self.remember_choice_checkbox.setChecked(self.hand_eye_configuration.show_dialog)
177+
150178
layout = QVBoxLayout()
151179
layout.addWidget(self.hand_eye_buttons)
152180
layout.addWidget(self.button_box)
181+
layout.addWidget(self.remember_choice_checkbox)
153182
self.setLayout(layout)
154183

155184
def accept(self):
156185
self.hand_eye_configuration = self.hand_eye_buttons.hand_eye_configuration
186+
self.hand_eye_configuration.show_dialog = self.remember_choice_checkbox.isChecked()
187+
self.hand_eye_configuration.save_choice()
157188
super().accept()
158189

159190

160191
def select_hand_eye_configuration(
161192
initial_hand_eye_configuration: HandEyeConfiguration = HandEyeConfiguration(),
193+
show_anyway: bool = False,
162194
) -> HandEyeConfiguration:
195+
if not show_anyway and not initial_hand_eye_configuration.show_dialog:
196+
return initial_hand_eye_configuration
163197
dialog = HandEyeConfigurationSelection(initial_hand_eye_configuration)
164198
dialog.exec_()
165199
return dialog.hand_eye_configuration
166200

167201

168202
if __name__ == "__main__": # NOLINT
169203
with ZividQtApplication():
170-
hand_eye_configuration = select_hand_eye_configuration()
171-
print(f"Selected settings: {hand_eye_configuration}")
204+
hand_eye_configuration = select_hand_eye_configuration(show_anyway=True)
205+
print(f"Selected Hand-Eye Configuration: {hand_eye_configuration}")

0 commit comments

Comments
 (0)