calibrate-suite 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- calibrate_suite-0.1.0.dist-info/METADATA +761 -0
- calibrate_suite-0.1.0.dist-info/RECORD +47 -0
- calibrate_suite-0.1.0.dist-info/WHEEL +5 -0
- calibrate_suite-0.1.0.dist-info/entry_points.txt +3 -0
- calibrate_suite-0.1.0.dist-info/licenses/LICENSE +201 -0
- calibrate_suite-0.1.0.dist-info/top_level.txt +4 -0
- fleet_server/__init__.py +32 -0
- fleet_server/app.py +377 -0
- fleet_server/config.py +91 -0
- fleet_server/templates/error.html +57 -0
- fleet_server/templates/index.html +137 -0
- fleet_server/templates/viewer.html +490 -0
- fleet_server/utils.py +178 -0
- gui/__init__.py +2 -0
- gui/assets/2d-or-3d-fleet-upload.png +0 -0
- gui/assets/2d_3d_overlay_output.jpg +0 -0
- gui/assets/3d-or-2d-overlay_page.png +0 -0
- gui/assets/3d-or-2d-record-page.png +0 -0
- gui/assets/3d_3d_overlay_output.png +0 -0
- gui/assets/3d_or_2d_calibrate-page.png +0 -0
- gui/assets/GUI_homepage.png +0 -0
- gui/assets/hardware_setup.jpeg +0 -0
- gui/assets/single_lidar_calibrate_page.png +0 -0
- gui/assets/single_lidar_output.png +0 -0
- gui/assets/single_lidar_record_page.png +0 -0
- gui/assets/virya.jpg +0 -0
- gui/main.py +23 -0
- gui/widgets/calibrator_widget.py +977 -0
- gui/widgets/extractor_widget.py +561 -0
- gui/widgets/home_widget.py +117 -0
- gui/widgets/recorder_widget.py +127 -0
- gui/widgets/single_lidar_widget.py +673 -0
- gui/widgets/three_d_calib_widget.py +87 -0
- gui/widgets/two_d_calib_widget.py +86 -0
- gui/widgets/uploader_widget.py +151 -0
- gui/widgets/validator_widget.py +614 -0
- gui/windows/main_window.py +56 -0
- gui/windows/main_window_ui.py +65 -0
- rviz_configs/2D-3D.rviz +183 -0
- rviz_configs/3D-3D.rviz +184 -0
- rviz_configs/default_calib.rviz +167 -0
- utils/__init__.py +13 -0
- utils/calibration_common.py +23 -0
- utils/cli_calibrate.py +53 -0
- utils/cli_fleet_server.py +64 -0
- utils/data_extractor_common.py +87 -0
- utils/gui_helpers.py +25 -0
|
@@ -0,0 +1,977 @@
|
|
|
1
|
+
from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
|
|
2
|
+
QLineEdit, QPushButton, QComboBox, QTextEdit, QMessageBox,
|
|
3
|
+
QGroupBox, QFileDialog, QProgressBar, QRadioButton, QButtonGroup,
|
|
4
|
+
QDoubleSpinBox, QGridLayout)
|
|
5
|
+
from PyQt5.QtCore import QProcess, Qt
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
class CalibratorWidget(QWidget):
|
|
10
|
+
def __init__(self):
|
|
11
|
+
super().__init__()
|
|
12
|
+
self.process = None
|
|
13
|
+
self.current_script = None
|
|
14
|
+
self.initUI()
|
|
15
|
+
|
|
16
|
+
def initUI(self):
|
|
17
|
+
main_layout = QVBoxLayout()
|
|
18
|
+
|
|
19
|
+
# ========== 2D Board Segmentation Parameters ==========
|
|
20
|
+
params_2d_group = QGroupBox("2D Board Segmentation Parameters (Optional)")
|
|
21
|
+
params_2d_layout = QGridLayout()
|
|
22
|
+
|
|
23
|
+
# Intrinsics file path
|
|
24
|
+
intrinsics_layout = QHBoxLayout()
|
|
25
|
+
self.intrinsics_input = QLineEdit()
|
|
26
|
+
self.intrinsics_input.setPlaceholderText("Path to camera_intrinsics.yaml (leave empty for default)")
|
|
27
|
+
btn_intrinsics_browse = QPushButton("Browse...")
|
|
28
|
+
btn_intrinsics_browse.setMaximumWidth(100)
|
|
29
|
+
btn_intrinsics_browse.clicked.connect(self.browse_intrinsics_file)
|
|
30
|
+
intrinsics_layout.addWidget(QLabel("Intrinsics File:"))
|
|
31
|
+
intrinsics_layout.addWidget(self.intrinsics_input)
|
|
32
|
+
intrinsics_layout.addWidget(btn_intrinsics_browse)
|
|
33
|
+
params_2d_layout.addLayout(intrinsics_layout, 0, 0, 1, 3)
|
|
34
|
+
|
|
35
|
+
# Tag Size (meters)
|
|
36
|
+
tag_size_layout = QHBoxLayout()
|
|
37
|
+
self.tag_size_input = QLineEdit()
|
|
38
|
+
self.tag_size_input.setText("0.04")
|
|
39
|
+
self.tag_size_input.setMaximumWidth(100)
|
|
40
|
+
self.tag_size_input.setToolTip("AprilTag size in meters (default: 0.04)")
|
|
41
|
+
tag_size_layout.addWidget(QLabel("Tag Size (m):"))
|
|
42
|
+
tag_size_layout.addWidget(self.tag_size_input)
|
|
43
|
+
tag_size_layout.addStretch()
|
|
44
|
+
params_2d_layout.addLayout(tag_size_layout, 1, 0)
|
|
45
|
+
|
|
46
|
+
# Tag Spacing (meters)
|
|
47
|
+
tag_spacing_layout = QHBoxLayout()
|
|
48
|
+
self.tag_spacing_input = QLineEdit()
|
|
49
|
+
self.tag_spacing_input.setText("0.01")
|
|
50
|
+
self.tag_spacing_input.setMaximumWidth(100)
|
|
51
|
+
self.tag_spacing_input.setToolTip("Spacing between tags in meters (default: 0.01)")
|
|
52
|
+
tag_spacing_layout.addWidget(QLabel("Tag Spacing (m):"))
|
|
53
|
+
tag_spacing_layout.addWidget(self.tag_spacing_input)
|
|
54
|
+
tag_spacing_layout.addStretch()
|
|
55
|
+
params_2d_layout.addLayout(tag_spacing_layout, 1, 1)
|
|
56
|
+
|
|
57
|
+
# Grid Rows
|
|
58
|
+
grid_rows_layout = QHBoxLayout()
|
|
59
|
+
self.grid_rows_input = QLineEdit()
|
|
60
|
+
self.grid_rows_input.setText("8")
|
|
61
|
+
self.grid_rows_input.setMaximumWidth(100)
|
|
62
|
+
self.grid_rows_input.setToolTip("Number of tag rows (default: 8)")
|
|
63
|
+
grid_rows_layout.addWidget(QLabel("Grid Rows:"))
|
|
64
|
+
grid_rows_layout.addWidget(self.grid_rows_input)
|
|
65
|
+
grid_rows_layout.addStretch()
|
|
66
|
+
params_2d_layout.addLayout(grid_rows_layout, 1, 2)
|
|
67
|
+
|
|
68
|
+
# Grid Cols
|
|
69
|
+
grid_cols_layout = QHBoxLayout()
|
|
70
|
+
self.grid_cols_input = QLineEdit()
|
|
71
|
+
self.grid_cols_input.setText("11")
|
|
72
|
+
self.grid_cols_input.setMaximumWidth(100)
|
|
73
|
+
self.grid_cols_input.setToolTip("Number of tag columns (default: 11)")
|
|
74
|
+
grid_cols_layout.addWidget(QLabel("Grid Cols:"))
|
|
75
|
+
grid_cols_layout.addWidget(self.grid_cols_input)
|
|
76
|
+
grid_cols_layout.addStretch()
|
|
77
|
+
params_2d_layout.addLayout(grid_cols_layout, 2, 0)
|
|
78
|
+
|
|
79
|
+
# Board Width (with margins)
|
|
80
|
+
board_w_layout = QHBoxLayout()
|
|
81
|
+
self.board_w_input = QLineEdit()
|
|
82
|
+
self.board_w_input.setText("0.609")
|
|
83
|
+
self.board_w_input.setMaximumWidth(100)
|
|
84
|
+
self.board_w_input.setToolTip("Board width in meters including white margins (default: 0.609)")
|
|
85
|
+
board_w_layout.addWidget(QLabel("Board Width (m):"))
|
|
86
|
+
board_w_layout.addWidget(self.board_w_input)
|
|
87
|
+
board_w_layout.addStretch()
|
|
88
|
+
params_2d_layout.addLayout(board_w_layout, 2, 1)
|
|
89
|
+
|
|
90
|
+
# Board Height (with margins)
|
|
91
|
+
board_h_layout = QHBoxLayout()
|
|
92
|
+
self.board_h_input = QLineEdit()
|
|
93
|
+
self.board_h_input.setText("0.457")
|
|
94
|
+
self.board_h_input.setMaximumWidth(100)
|
|
95
|
+
self.board_h_input.setToolTip("Board height in meters including white margins (default: 0.457)")
|
|
96
|
+
board_h_layout.addWidget(QLabel("Board Height (m):"))
|
|
97
|
+
board_h_layout.addWidget(self.board_h_input)
|
|
98
|
+
board_h_layout.addStretch()
|
|
99
|
+
params_2d_layout.addLayout(board_h_layout, 2, 2)
|
|
100
|
+
|
|
101
|
+
params_2d_group.setLayout(params_2d_layout)
|
|
102
|
+
main_layout.addWidget(params_2d_group)
|
|
103
|
+
|
|
104
|
+
# ========== Configuration Section ==========
|
|
105
|
+
config_group = QGroupBox("Configuration")
|
|
106
|
+
config_layout = QVBoxLayout()
|
|
107
|
+
|
|
108
|
+
# Data Root Directory
|
|
109
|
+
data_root_layout = QHBoxLayout()
|
|
110
|
+
self.data_root_input = QLineEdit()
|
|
111
|
+
cwd = os.getcwd()
|
|
112
|
+
# Default to calib_data/new_captures
|
|
113
|
+
default_root = os.path.join(cwd, "calib_data/new_captures")
|
|
114
|
+
self.data_root_input.setText(default_root)
|
|
115
|
+
self.data_root_input.setPlaceholderText("Path to calib_data (e.g., calib_data/new_captures)")
|
|
116
|
+
|
|
117
|
+
btn_browse = QPushButton("Browse...")
|
|
118
|
+
btn_browse.setMaximumWidth(100)
|
|
119
|
+
btn_browse.clicked.connect(self.browse_data_root)
|
|
120
|
+
|
|
121
|
+
data_root_layout.addWidget(QLabel("Data Root:"))
|
|
122
|
+
data_root_layout.addWidget(self.data_root_input)
|
|
123
|
+
data_root_layout.addWidget(btn_browse)
|
|
124
|
+
config_layout.addLayout(data_root_layout)
|
|
125
|
+
|
|
126
|
+
# Best N Poses (for calibration quality filter)
|
|
127
|
+
best_n_layout = QHBoxLayout()
|
|
128
|
+
self.best_n_input = QLineEdit()
|
|
129
|
+
self.best_n_input.setText("15")
|
|
130
|
+
self.best_n_input.setMaximumWidth(100)
|
|
131
|
+
best_n_layout.addWidget(QLabel("Best N Poses:"))
|
|
132
|
+
best_n_layout.addWidget(self.best_n_input)
|
|
133
|
+
best_n_layout.addStretch()
|
|
134
|
+
config_layout.addLayout(best_n_layout)
|
|
135
|
+
|
|
136
|
+
config_group.setLayout(config_layout)
|
|
137
|
+
main_layout.addWidget(config_group)
|
|
138
|
+
|
|
139
|
+
# ========== Calibration Steps Section ==========
|
|
140
|
+
steps_group = QGroupBox("Calibration Workflow")
|
|
141
|
+
steps_layout = QVBoxLayout()
|
|
142
|
+
|
|
143
|
+
# Step 1: Camera Detection
|
|
144
|
+
step1_layout = QHBoxLayout()
|
|
145
|
+
step1_label = QLabel("Step 1: Camera Detection (AprilTag Corners)")
|
|
146
|
+
step1_label.setStyleSheet("font-weight: bold; color: #1976D2;")
|
|
147
|
+
self.btn_camtag = QPushButton("Run Camera Detection")
|
|
148
|
+
self.btn_camtag.setStyleSheet("""
|
|
149
|
+
QPushButton { background-color: #1976D2; color: white; padding: 8px; font-weight: bold; border-radius: 4px; }
|
|
150
|
+
QPushButton:hover { background-color: #1565C0; }
|
|
151
|
+
QPushButton:disabled { background-color: #BDBDBD; }
|
|
152
|
+
""")
|
|
153
|
+
self.btn_camtag.clicked.connect(self.run_camera_detection)
|
|
154
|
+
step1_layout.addWidget(step1_label)
|
|
155
|
+
step1_layout.addStretch()
|
|
156
|
+
step1_layout.addWidget(self.btn_camtag)
|
|
157
|
+
steps_layout.addLayout(step1_layout)
|
|
158
|
+
|
|
159
|
+
# Step 2: LiDAR Detection
|
|
160
|
+
step2_layout = QHBoxLayout()
|
|
161
|
+
step2_label = QLabel("Step 2: LiDAR Detection (Board Corners)")
|
|
162
|
+
step2_label.setStyleSheet("font-weight: bold; color: #388E3C;")
|
|
163
|
+
self.btn_lidtag = QPushButton("Run LiDAR Detection")
|
|
164
|
+
self.btn_lidtag.setStyleSheet("""
|
|
165
|
+
QPushButton { background-color: #388E3C; color: white; padding: 8px; font-weight: bold; border-radius: 4px; }
|
|
166
|
+
QPushButton:hover { background-color: #2E7D32; }
|
|
167
|
+
QPushButton:disabled { background-color: #BDBDBD; }
|
|
168
|
+
""")
|
|
169
|
+
self.btn_lidtag.clicked.connect(lambda: self.run_script("2d_board_segmentation.py", "--lidar"))
|
|
170
|
+
step2_layout.addWidget(step2_label)
|
|
171
|
+
step2_layout.addStretch()
|
|
172
|
+
step2_layout.addWidget(self.btn_lidtag)
|
|
173
|
+
steps_layout.addLayout(step2_layout)
|
|
174
|
+
|
|
175
|
+
# Step 3: Calibration
|
|
176
|
+
step3_layout = QHBoxLayout()
|
|
177
|
+
step3_label = QLabel("Step 3: Extrinsic Calibration (Optimization)")
|
|
178
|
+
step3_label.setStyleSheet("font-weight: bold; color: #D32F2F;")
|
|
179
|
+
self.btn_aprilboard = QPushButton("Run Calibration")
|
|
180
|
+
self.btn_aprilboard.setStyleSheet("""
|
|
181
|
+
QPushButton { background-color: #D32F2F; color: white; padding: 8px; font-weight: bold; border-radius: 4px; }
|
|
182
|
+
QPushButton:hover { background-color: #C62828; }
|
|
183
|
+
QPushButton:disabled { background-color: #BDBDBD; }
|
|
184
|
+
""")
|
|
185
|
+
self.btn_aprilboard.clicked.connect(lambda: self.run_script("2d_3d_calibrator.py"))
|
|
186
|
+
step3_layout.addWidget(step3_label)
|
|
187
|
+
step3_layout.addStretch()
|
|
188
|
+
step3_layout.addWidget(self.btn_aprilboard)
|
|
189
|
+
steps_layout.addLayout(step3_layout)
|
|
190
|
+
|
|
191
|
+
steps_group.setLayout(steps_layout)
|
|
192
|
+
main_layout.addWidget(steps_group)
|
|
193
|
+
|
|
194
|
+
# ========== Progress Bar ===========
|
|
195
|
+
self.progress_bar = QProgressBar()
|
|
196
|
+
self.progress_bar.setVisible(False)
|
|
197
|
+
main_layout.addWidget(self.progress_bar)
|
|
198
|
+
|
|
199
|
+
# ========== Log Output ==========
|
|
200
|
+
log_group = QGroupBox("Log Output")
|
|
201
|
+
log_layout = QVBoxLayout()
|
|
202
|
+
self.log_area = QTextEdit()
|
|
203
|
+
self.log_area.setReadOnly(True)
|
|
204
|
+
self.log_area.setStyleSheet("font-family: Monospace; font-size: 9pt;")
|
|
205
|
+
self.log_area.setMinimumHeight(400)
|
|
206
|
+
log_layout.addWidget(self.log_area)
|
|
207
|
+
log_group.setLayout(log_layout)
|
|
208
|
+
main_layout.addWidget(log_group, 1) # Give log more space
|
|
209
|
+
|
|
210
|
+
# ========== Quick Action Buttons ==========
|
|
211
|
+
action_layout = QHBoxLayout()
|
|
212
|
+
|
|
213
|
+
btn_run_all = QPushButton("Run All Steps (Sequential)")
|
|
214
|
+
btn_run_all.setStyleSheet("""
|
|
215
|
+
QPushButton { background-color: #FF9800; color: white; padding: 10px; font-weight: bold; border-radius: 4px; }
|
|
216
|
+
QPushButton:hover { background-color: #F57C00; }
|
|
217
|
+
""")
|
|
218
|
+
btn_run_all.clicked.connect(self.run_all_steps)
|
|
219
|
+
|
|
220
|
+
btn_clear_log = QPushButton("Clear Log")
|
|
221
|
+
btn_clear_log.setMaximumWidth(100)
|
|
222
|
+
btn_clear_log.clicked.connect(self.log_area.clear)
|
|
223
|
+
|
|
224
|
+
action_layout.addWidget(btn_run_all)
|
|
225
|
+
action_layout.addStretch()
|
|
226
|
+
action_layout.addWidget(btn_clear_log)
|
|
227
|
+
main_layout.addLayout(action_layout, 0)
|
|
228
|
+
|
|
229
|
+
main_layout.addStretch()
|
|
230
|
+
self.setLayout(main_layout)
|
|
231
|
+
|
|
232
|
+
def browse_data_root(self):
|
|
233
|
+
"""Open file browser to select data root directory"""
|
|
234
|
+
folder = QFileDialog.getExistingDirectory(self, "Select Data Root Directory")
|
|
235
|
+
if folder:
|
|
236
|
+
self.data_root_input.setText(folder)
|
|
237
|
+
|
|
238
|
+
def browse_intrinsics_file(self):
|
|
239
|
+
"""Open file browser to select camera intrinsics YAML file"""
|
|
240
|
+
file_path, _ = QFileDialog.getOpenFileName(
|
|
241
|
+
self,
|
|
242
|
+
"Select Camera Intrinsics File",
|
|
243
|
+
"",
|
|
244
|
+
"YAML Files (*.yaml *.yml);;All Files (*)"
|
|
245
|
+
)
|
|
246
|
+
if file_path:
|
|
247
|
+
self.intrinsics_input.setText(file_path)
|
|
248
|
+
|
|
249
|
+
def get_2d_board_args(self):
|
|
250
|
+
"""Build 2D board segmentation arguments from GUI inputs"""
|
|
251
|
+
args = []
|
|
252
|
+
|
|
253
|
+
# Intrinsics file (optional)
|
|
254
|
+
intrinsics = self.intrinsics_input.text().strip()
|
|
255
|
+
if intrinsics:
|
|
256
|
+
args.extend(["--intrinsics", intrinsics])
|
|
257
|
+
|
|
258
|
+
# Tag size (optional, skip if default)
|
|
259
|
+
tag_size = self.tag_size_input.text().strip()
|
|
260
|
+
if tag_size and tag_size != "0.04":
|
|
261
|
+
args.extend(["--tag_size", tag_size])
|
|
262
|
+
|
|
263
|
+
# Tag spacing (optional, skip if default)
|
|
264
|
+
tag_spacing = self.tag_spacing_input.text().strip()
|
|
265
|
+
if tag_spacing and tag_spacing != "0.01":
|
|
266
|
+
args.extend(["--tag_spacing", tag_spacing])
|
|
267
|
+
|
|
268
|
+
# Grid rows (optional, skip if default)
|
|
269
|
+
grid_rows = self.grid_rows_input.text().strip()
|
|
270
|
+
if grid_rows and grid_rows != "8":
|
|
271
|
+
args.extend(["--grid_rows", grid_rows])
|
|
272
|
+
|
|
273
|
+
# Grid cols (optional, skip if default)
|
|
274
|
+
grid_cols = self.grid_cols_input.text().strip()
|
|
275
|
+
if grid_cols and grid_cols != "11":
|
|
276
|
+
args.extend(["--grid_cols", grid_cols])
|
|
277
|
+
|
|
278
|
+
# Board width (optional, skip if default)
|
|
279
|
+
board_w = self.board_w_input.text().strip()
|
|
280
|
+
if board_w and board_w != "0.609":
|
|
281
|
+
args.extend(["--board_w", board_w])
|
|
282
|
+
|
|
283
|
+
# Board height (optional, skip if default)
|
|
284
|
+
board_h = self.board_h_input.text().strip()
|
|
285
|
+
if board_h and board_h != "0.457":
|
|
286
|
+
args.extend(["--board_h", board_h])
|
|
287
|
+
|
|
288
|
+
return args
|
|
289
|
+
|
|
290
|
+
def run_camera_detection(self):
|
|
291
|
+
"""Run camera detection with configured parameters"""
|
|
292
|
+
board_args = self.get_2d_board_args()
|
|
293
|
+
camera_args = ["--camera"] + board_args
|
|
294
|
+
self.run_script("2d_board_segmentation.py", camera_args)
|
|
295
|
+
|
|
296
|
+
def run_script(self, script_name, script_args=""):
|
|
297
|
+
"""Run a calibration script with optional arguments (args can be string or list)"""
|
|
298
|
+
data_root = self.data_root_input.text()
|
|
299
|
+
best_n = self.best_n_input.text()
|
|
300
|
+
|
|
301
|
+
if not data_root or not os.path.exists(data_root):
|
|
302
|
+
QMessageBox.warning(self, "Error", f"Invalid data root: {data_root}")
|
|
303
|
+
return
|
|
304
|
+
|
|
305
|
+
# Disable all buttons during execution
|
|
306
|
+
self.set_buttons_enabled(False)
|
|
307
|
+
self.log_area.clear()
|
|
308
|
+
self.log_area.append(f"🚀 Running {script_name}...")
|
|
309
|
+
self.log_area.append(f" Data Root: {data_root}")
|
|
310
|
+
|
|
311
|
+
# Display arguments (convert list to string for display)
|
|
312
|
+
if script_args:
|
|
313
|
+
if isinstance(script_args, list):
|
|
314
|
+
args_display = " ".join(script_args)
|
|
315
|
+
else:
|
|
316
|
+
args_display = script_args
|
|
317
|
+
self.log_area.append(f" Arguments: {args_display}")
|
|
318
|
+
|
|
319
|
+
if script_name == "2d_3d_calibrator.py":
|
|
320
|
+
self.log_area.append(f" Best N Poses: {best_n}")
|
|
321
|
+
self.log_area.append("")
|
|
322
|
+
|
|
323
|
+
self.current_script = script_name
|
|
324
|
+
self.process = QProcess()
|
|
325
|
+
self.process.readyReadStandardOutput.connect(self.handle_stdout)
|
|
326
|
+
self.process.readyReadStandardError.connect(self.handle_stderr)
|
|
327
|
+
self.process.finished.connect(self.on_finished)
|
|
328
|
+
|
|
329
|
+
# Get script path
|
|
330
|
+
script_path = os.path.abspath(os.path.join(os.path.dirname(__file__), f"../../{script_name}"))
|
|
331
|
+
|
|
332
|
+
if not os.path.exists(script_path):
|
|
333
|
+
self.log_area.append(f"❌ Script not found: {script_path}")
|
|
334
|
+
self.set_buttons_enabled(True)
|
|
335
|
+
return
|
|
336
|
+
|
|
337
|
+
cmd = sys.executable # Use the same Python interpreter
|
|
338
|
+
args = [script_path]
|
|
339
|
+
|
|
340
|
+
# Add script-specific arguments (handle both list and string)
|
|
341
|
+
if script_args:
|
|
342
|
+
if isinstance(script_args, list):
|
|
343
|
+
args.extend(script_args)
|
|
344
|
+
else:
|
|
345
|
+
args.extend(script_args.split())
|
|
346
|
+
|
|
347
|
+
# Always add data_root
|
|
348
|
+
args.extend(["--data_root", data_root])
|
|
349
|
+
|
|
350
|
+
if script_name == "2d_3d_calibrator.py":
|
|
351
|
+
args.extend(["--best_n", best_n])
|
|
352
|
+
|
|
353
|
+
self.process.start(cmd, args)
|
|
354
|
+
|
|
355
|
+
def run_all_steps(self):
|
|
356
|
+
"""Run all 3 steps sequentially"""
|
|
357
|
+
msg_box = QMessageBox(self)
|
|
358
|
+
msg_box.setWindowTitle("Sequential Execution")
|
|
359
|
+
msg_box.setText(
|
|
360
|
+
"Running all steps sequentially:\n"
|
|
361
|
+
"1. Camera Detection\n"
|
|
362
|
+
"2. LiDAR Detection\n"
|
|
363
|
+
"3. 2D_3D_Calibration\n\n"
|
|
364
|
+
"This will take a few minutes...")
|
|
365
|
+
msg_box.setIcon(QMessageBox.Information)
|
|
366
|
+
msg_box.setStandardButtons(QMessageBox.Ok)
|
|
367
|
+
|
|
368
|
+
# Style the dialog with dark background
|
|
369
|
+
msg_box.setStyleSheet("""
|
|
370
|
+
QMessageBox {
|
|
371
|
+
background-color: #2B2B2B;
|
|
372
|
+
}
|
|
373
|
+
QMessageBox QLabel {
|
|
374
|
+
color: #FFFFFF;
|
|
375
|
+
}
|
|
376
|
+
QPushButton {
|
|
377
|
+
background-color: #4CAF50;
|
|
378
|
+
color: white;
|
|
379
|
+
padding: 6px 20px;
|
|
380
|
+
border-radius: 4px;
|
|
381
|
+
font-weight: bold;
|
|
382
|
+
min-width: 60px;
|
|
383
|
+
}
|
|
384
|
+
QPushButton:hover {
|
|
385
|
+
background-color: #45a049;
|
|
386
|
+
}
|
|
387
|
+
""")
|
|
388
|
+
|
|
389
|
+
msg_box.exec_()
|
|
390
|
+
|
|
391
|
+
# Build 2D board segmentation args from GUI
|
|
392
|
+
board_args = self.get_2d_board_args()
|
|
393
|
+
camera_args = ["--camera"] + board_args
|
|
394
|
+
lidar_args = ["--lidar"] + board_args
|
|
395
|
+
|
|
396
|
+
self.steps_queue = [
|
|
397
|
+
("2d_board_segmentation.py", camera_args),
|
|
398
|
+
("2d_board_segmentation.py", lidar_args),
|
|
399
|
+
("2d_3d_calibrator.py", "")
|
|
400
|
+
]
|
|
401
|
+
self.current_step_index = 0
|
|
402
|
+
self.run_next_step()
|
|
403
|
+
|
|
404
|
+
def run_next_step(self):
|
|
405
|
+
"""Run the next step in the queue"""
|
|
406
|
+
if self.current_step_index < len(self.steps_queue):
|
|
407
|
+
script, script_args = self.steps_queue[self.current_step_index]
|
|
408
|
+
step_num = self.current_step_index + 1
|
|
409
|
+
self.log_area.append(f"\n{'='*80}")
|
|
410
|
+
if script_args:
|
|
411
|
+
self.log_area.append(f"STEP {step_num}/3: {script} {script_args}")
|
|
412
|
+
else:
|
|
413
|
+
self.log_area.append(f"STEP {step_num}/3: {script}")
|
|
414
|
+
self.log_area.append(f"{'='*80}\n")
|
|
415
|
+
self.run_script(script, script_args)
|
|
416
|
+
else:
|
|
417
|
+
self.log_area.append(f"\n{'='*80}")
|
|
418
|
+
self.log_area.append("✅ ALL STEPS COMPLETED SUCCESSFULLY!")
|
|
419
|
+
self.log_area.append(f"{'='*80}\n")
|
|
420
|
+
self.set_buttons_enabled(True)
|
|
421
|
+
|
|
422
|
+
def handle_stdout(self):
|
|
423
|
+
"""Handle stdout from process"""
|
|
424
|
+
data = self.process.readAllStandardOutput()
|
|
425
|
+
text = data.data().decode()
|
|
426
|
+
self.log_area.append(text)
|
|
427
|
+
|
|
428
|
+
def handle_stderr(self):
|
|
429
|
+
"""Handle stderr from process"""
|
|
430
|
+
data = self.process.readAllStandardError()
|
|
431
|
+
text = data.data().decode()
|
|
432
|
+
self.log_area.append(f"<span style='color:red'>{text}</span>")
|
|
433
|
+
|
|
434
|
+
def on_finished(self):
|
|
435
|
+
"""Handle process completion"""
|
|
436
|
+
exit_code = self.process.exitCode()
|
|
437
|
+
|
|
438
|
+
if exit_code == 0:
|
|
439
|
+
self.log_area.append(f"\n✅ {self.current_script} completed successfully!")
|
|
440
|
+
else:
|
|
441
|
+
self.log_area.append(f"\n❌ {self.current_script} failed with exit code {exit_code}")
|
|
442
|
+
|
|
443
|
+
# Check if we have more steps to run
|
|
444
|
+
if hasattr(self, 'steps_queue'):
|
|
445
|
+
if exit_code != 0:
|
|
446
|
+
# Step failed - show error and stop
|
|
447
|
+
self.set_buttons_enabled(True)
|
|
448
|
+
msg_box = QMessageBox(self)
|
|
449
|
+
msg_box.setWindowTitle("Error")
|
|
450
|
+
msg_box.setText(f"Step {self.current_step_index + 1} failed. Check logs for details.")
|
|
451
|
+
msg_box.setIcon(QMessageBox.Critical)
|
|
452
|
+
msg_box.setStyleSheet("""
|
|
453
|
+
QMessageBox {
|
|
454
|
+
background-color: #3A3A3A;
|
|
455
|
+
}
|
|
456
|
+
QMessageBox QLabel {
|
|
457
|
+
color: #FFFFFF;
|
|
458
|
+
}
|
|
459
|
+
QPushButton {
|
|
460
|
+
background-color: #f44336;
|
|
461
|
+
color: white;
|
|
462
|
+
padding: 6px 20px;
|
|
463
|
+
border-radius: 4px;
|
|
464
|
+
font-weight: bold;
|
|
465
|
+
min-width: 60px;
|
|
466
|
+
}
|
|
467
|
+
QPushButton:hover {
|
|
468
|
+
background-color: #da190b;
|
|
469
|
+
}
|
|
470
|
+
""")
|
|
471
|
+
msg_box.exec_()
|
|
472
|
+
return
|
|
473
|
+
|
|
474
|
+
self.current_step_index += 1
|
|
475
|
+
if self.current_step_index < len(self.steps_queue):
|
|
476
|
+
self.run_next_step()
|
|
477
|
+
else:
|
|
478
|
+
self.log_area.append(f"\n{'='*80}")
|
|
479
|
+
self.log_area.append("✅ ALL STEPS COMPLETED!")
|
|
480
|
+
self.log_area.append(f"{'='*80}\n")
|
|
481
|
+
self.set_buttons_enabled(True)
|
|
482
|
+
else:
|
|
483
|
+
self.set_buttons_enabled(True)
|
|
484
|
+
|
|
485
|
+
def set_buttons_enabled(self, enabled):
|
|
486
|
+
"""Enable/disable all action buttons"""
|
|
487
|
+
self.btn_camtag.setEnabled(enabled)
|
|
488
|
+
self.btn_lidtag.setEnabled(enabled)
|
|
489
|
+
self.btn_aprilboard.setEnabled(enabled)
|
|
490
|
+
|
|
491
|
+
# 3D-3D CALIBRATOR WIDGET (Separate from 2D-3D)
|
|
492
|
+
|
|
493
|
+
class ThreeDCalibratorWidget(QWidget):
|
|
494
|
+
"""3D-3D Calibration using point cloud segmentation and optimization"""
|
|
495
|
+
|
|
496
|
+
def __init__(self):
|
|
497
|
+
super().__init__()
|
|
498
|
+
self.process = None
|
|
499
|
+
self.current_script = None
|
|
500
|
+
self.initUI()
|
|
501
|
+
|
|
502
|
+
def initUI(self):
|
|
503
|
+
# Apply global style for inputs in this widget
|
|
504
|
+
self.setStyleSheet("""
|
|
505
|
+
QDoubleSpinBox {
|
|
506
|
+
background-color: #424242;
|
|
507
|
+
color: white;
|
|
508
|
+
padding: 4px;
|
|
509
|
+
border: 1px solid #666;
|
|
510
|
+
border-radius: 4px;
|
|
511
|
+
selection-background-color: #1976D2;
|
|
512
|
+
}
|
|
513
|
+
QDoubleSpinBox:focus {
|
|
514
|
+
border: 1px solid #4CAF50;
|
|
515
|
+
}
|
|
516
|
+
QDoubleSpinBox::up-button, QDoubleSpinBox::down-button {
|
|
517
|
+
background-color: #555;
|
|
518
|
+
border-radius: 2px;
|
|
519
|
+
width: 16px;
|
|
520
|
+
}
|
|
521
|
+
QDoubleSpinBox::up-button:hover, QDoubleSpinBox::down-button:hover {
|
|
522
|
+
background-color: #777;
|
|
523
|
+
}
|
|
524
|
+
QComboBox {
|
|
525
|
+
background-color: #424242;
|
|
526
|
+
color: white;
|
|
527
|
+
padding: 4px;
|
|
528
|
+
border: 1px solid #666;
|
|
529
|
+
border-radius: 4px;
|
|
530
|
+
}
|
|
531
|
+
QComboBox:on {
|
|
532
|
+
background-color: #424242;
|
|
533
|
+
}
|
|
534
|
+
QComboBox QAbstractItemView {
|
|
535
|
+
background-color: #424242;
|
|
536
|
+
color: white;
|
|
537
|
+
selection-background-color: #1976D2;
|
|
538
|
+
border: 1px solid #666;
|
|
539
|
+
}
|
|
540
|
+
QLineEdit {
|
|
541
|
+
background-color: #424242;
|
|
542
|
+
color: white;
|
|
543
|
+
padding: 4px;
|
|
544
|
+
border: 1px solid #666;
|
|
545
|
+
border-radius: 4px;
|
|
546
|
+
}
|
|
547
|
+
""")
|
|
548
|
+
|
|
549
|
+
main_layout = QVBoxLayout()
|
|
550
|
+
|
|
551
|
+
# ========== Root Folder Selection (Top-level) ==========
|
|
552
|
+
root_group = QGroupBox("Data Management")
|
|
553
|
+
root_layout = QVBoxLayout()
|
|
554
|
+
|
|
555
|
+
# Data Root Directory
|
|
556
|
+
data_root_layout = QHBoxLayout()
|
|
557
|
+
self.data_root_input = QLineEdit()
|
|
558
|
+
cwd = os.getcwd()
|
|
559
|
+
# Default to calib_data/cb_data for 3D-3D
|
|
560
|
+
default_root = os.path.join(cwd, "calib_data/cb_data")
|
|
561
|
+
self.data_root_input.setText(default_root)
|
|
562
|
+
self.data_root_input.setPlaceholderText("Path to calib_data (e.g., calib_data/cb_data)")
|
|
563
|
+
|
|
564
|
+
btn_browse = QPushButton("Browse...")
|
|
565
|
+
btn_browse.setMaximumWidth(100)
|
|
566
|
+
btn_browse.clicked.connect(self.browse_data_root)
|
|
567
|
+
|
|
568
|
+
data_root_layout.addWidget(QLabel("Root Directory:"))
|
|
569
|
+
data_root_layout.addWidget(self.data_root_input)
|
|
570
|
+
data_root_layout.addWidget(btn_browse)
|
|
571
|
+
root_layout.addLayout(data_root_layout)
|
|
572
|
+
|
|
573
|
+
root_group.setLayout(root_layout)
|
|
574
|
+
main_layout.addWidget(root_group)
|
|
575
|
+
|
|
576
|
+
# ========== Board Segmentation Section ==========
|
|
577
|
+
segmentation_group = QGroupBox("Board Segmentation Parameters")
|
|
578
|
+
segmentation_layout = QHBoxLayout() # Side-by-side layout for sensors
|
|
579
|
+
|
|
580
|
+
# --- LiDAR Panel ---
|
|
581
|
+
lidar_panel = QGroupBox("LiDAR Settings")
|
|
582
|
+
lidar_panel.setStyleSheet("QGroupBox { border: 1px solid #4CAF50; border-radius: 6px; margin-top: 10px; } QGroupBox::title { color: #4CAF50; }")
|
|
583
|
+
lidar_layout = QVBoxLayout()
|
|
584
|
+
|
|
585
|
+
# LiDAR Axis Info (Compact)
|
|
586
|
+
l_axis_label = QLabel("<b>Axis (ROS):</b> X=Fwd, Y=Left, Z=Up")
|
|
587
|
+
l_axis_label.setStyleSheet("color: #AAA; font-size: 9pt;") # Lighter gray for better visibility on dark bg
|
|
588
|
+
lidar_layout.addWidget(l_axis_label)
|
|
589
|
+
|
|
590
|
+
# LiDAR BBox
|
|
591
|
+
self.lidar_inputs = {'min': {}, 'max': {}}
|
|
592
|
+
l_grid = QGridLayout()
|
|
593
|
+
l_grid.setHorizontalSpacing(5)
|
|
594
|
+
l_grid.setVerticalSpacing(2)
|
|
595
|
+
|
|
596
|
+
axes = ['X', 'Y', 'Z']
|
|
597
|
+
defaults_l = {'min': [-0.5, -0.8, -0.4], 'max': [2.3, 0.9, 1.0]}
|
|
598
|
+
|
|
599
|
+
for i, axis in enumerate(axes):
|
|
600
|
+
l_grid.addWidget(QLabel(f"{axis} Min:"), i, 0)
|
|
601
|
+
self.lidar_inputs['min'][axis] = QDoubleSpinBox()
|
|
602
|
+
self.lidar_inputs['min'][axis].setRange(-10, 10)
|
|
603
|
+
self.lidar_inputs['min'][axis].setSingleStep(0.1)
|
|
604
|
+
self.lidar_inputs['min'][axis].setValue(defaults_l['min'][i])
|
|
605
|
+
l_grid.addWidget(self.lidar_inputs['min'][axis], i, 1)
|
|
606
|
+
|
|
607
|
+
l_grid.addWidget(QLabel(f"{axis} Max:"), i, 2)
|
|
608
|
+
self.lidar_inputs['max'][axis] = QDoubleSpinBox()
|
|
609
|
+
self.lidar_inputs['max'][axis].setRange(-10, 10)
|
|
610
|
+
self.lidar_inputs['max'][axis].setSingleStep(0.1)
|
|
611
|
+
self.lidar_inputs['max'][axis].setValue(defaults_l['max'][i])
|
|
612
|
+
l_grid.addWidget(self.lidar_inputs['max'][axis], i, 3)
|
|
613
|
+
|
|
614
|
+
lidar_layout.addLayout(l_grid)
|
|
615
|
+
|
|
616
|
+
# Run Button
|
|
617
|
+
btn_lidar = QPushButton("Run LiDAR Seg")
|
|
618
|
+
btn_lidar.setStyleSheet("""
|
|
619
|
+
QPushButton { background-color: #388E3C; color: white; padding: 5px; border-radius: 4px; }
|
|
620
|
+
QPushButton:hover { background-color: #4CAF50; }
|
|
621
|
+
""")
|
|
622
|
+
btn_lidar.clicked.connect(lambda: self.run_extract_board('lidar'))
|
|
623
|
+
lidar_layout.addWidget(btn_lidar)
|
|
624
|
+
lidar_panel.setLayout(lidar_layout)
|
|
625
|
+
segmentation_layout.addWidget(lidar_panel)
|
|
626
|
+
|
|
627
|
+
# --- Camera Panel ---
|
|
628
|
+
camera_panel = QGroupBox("Camera Settings")
|
|
629
|
+
camera_panel.setStyleSheet("QGroupBox { border: 1px solid #1976D2; border-radius: 6px; margin-top: 10px; } QGroupBox::title { color: #1976D2; }")
|
|
630
|
+
camera_layout = QVBoxLayout()
|
|
631
|
+
|
|
632
|
+
# Camera Axis Info (Compact)
|
|
633
|
+
c_axis_label = QLabel("<b>Axis (Optical):</b> X=Right, Y=Down, Z=Fwd")
|
|
634
|
+
c_axis_label.setStyleSheet("color: #AAA; font-size: 9pt;")
|
|
635
|
+
camera_layout.addWidget(c_axis_label)
|
|
636
|
+
|
|
637
|
+
# Camera BBox
|
|
638
|
+
self.camera_inputs = {'min': {}, 'max': {}}
|
|
639
|
+
c_grid = QGridLayout()
|
|
640
|
+
c_grid.setHorizontalSpacing(5)
|
|
641
|
+
c_grid.setVerticalSpacing(2)
|
|
642
|
+
|
|
643
|
+
defaults_c = {'min': [-0.9, -0.68, 0.3], 'max': [0.5, 1.0, 2.5]}
|
|
644
|
+
|
|
645
|
+
for i, axis in enumerate(axes):
|
|
646
|
+
c_grid.addWidget(QLabel(f"{axis} Min:"), i, 0)
|
|
647
|
+
self.camera_inputs['min'][axis] = QDoubleSpinBox()
|
|
648
|
+
self.camera_inputs['min'][axis].setRange(-10, 10)
|
|
649
|
+
self.camera_inputs['min'][axis].setSingleStep(0.1)
|
|
650
|
+
self.camera_inputs['min'][axis].setValue(defaults_c['min'][i])
|
|
651
|
+
c_grid.addWidget(self.camera_inputs['min'][axis], i, 1)
|
|
652
|
+
|
|
653
|
+
c_grid.addWidget(QLabel(f"{axis} Max:"), i, 2)
|
|
654
|
+
self.camera_inputs['max'][axis] = QDoubleSpinBox()
|
|
655
|
+
self.camera_inputs['max'][axis].setRange(-10, 10)
|
|
656
|
+
self.camera_inputs['max'][axis].setSingleStep(0.1)
|
|
657
|
+
self.camera_inputs['max'][axis].setValue(defaults_c['max'][i])
|
|
658
|
+
c_grid.addWidget(self.camera_inputs['max'][axis], i, 3)
|
|
659
|
+
|
|
660
|
+
camera_layout.addLayout(c_grid)
|
|
661
|
+
|
|
662
|
+
# Run Button
|
|
663
|
+
btn_camera = QPushButton("Run Camera Seg")
|
|
664
|
+
btn_camera.setStyleSheet("""
|
|
665
|
+
QPushButton { background-color: #1976D2; color: white; padding: 5px; border-radius: 4px; }
|
|
666
|
+
QPushButton:hover { background-color: #2196F3; }
|
|
667
|
+
""")
|
|
668
|
+
btn_camera.clicked.connect(lambda: self.run_extract_board('camera'))
|
|
669
|
+
camera_layout.addWidget(btn_camera)
|
|
670
|
+
camera_panel.setLayout(camera_layout)
|
|
671
|
+
segmentation_layout.addWidget(camera_panel)
|
|
672
|
+
|
|
673
|
+
segmentation_group.setLayout(segmentation_layout)
|
|
674
|
+
main_layout.addWidget(segmentation_group)
|
|
675
|
+
|
|
676
|
+
# ========== Calibration Method ==========
|
|
677
|
+
calib_group = QGroupBox("Calibration Algorithm")
|
|
678
|
+
calib_layout = QHBoxLayout()
|
|
679
|
+
|
|
680
|
+
calib_layout.addWidget(QLabel("Method:"))
|
|
681
|
+
self.method_combo = QComboBox()
|
|
682
|
+
self.method_combo.addItems([
|
|
683
|
+
"Umeyama (SVD only)",
|
|
684
|
+
"Optimization (Basin Hopping)",
|
|
685
|
+
"Both (Sequential Refinement)"
|
|
686
|
+
])
|
|
687
|
+
self.method_combo.setCurrentIndex(2) # Default to Both
|
|
688
|
+
calib_layout.addWidget(self.method_combo)
|
|
689
|
+
|
|
690
|
+
btn_run_calib = QPushButton("Run Calibration Only")
|
|
691
|
+
btn_run_calib.setStyleSheet("""
|
|
692
|
+
QPushButton { background-color: #D32F2F; color: white; padding: 10px; font-weight: bold; border-radius: 4px; }
|
|
693
|
+
QPushButton:hover { background-color: #C62828; }
|
|
694
|
+
""")
|
|
695
|
+
btn_run_calib.clicked.connect(self.run_calibration)
|
|
696
|
+
calib_layout.addWidget(btn_run_calib)
|
|
697
|
+
|
|
698
|
+
calib_group.setLayout(calib_layout)
|
|
699
|
+
main_layout.addWidget(calib_group)
|
|
700
|
+
|
|
701
|
+
# ========== Quick Action ==========
|
|
702
|
+
action_group = QGroupBox("Batch Processing")
|
|
703
|
+
action_layout_box = QHBoxLayout()
|
|
704
|
+
|
|
705
|
+
btn_run_all = QPushButton("Run All Sequentially (Cam Seg -> LiDAR Seg -> Calibration)")
|
|
706
|
+
btn_run_all.setStyleSheet("""
|
|
707
|
+
QPushButton { background-color: #FF9800; color: white; padding: 12px; font-weight: bold; border-radius: 6px; font-size: 11pt; }
|
|
708
|
+
QPushButton:hover { background-color: #F57C00; }
|
|
709
|
+
""")
|
|
710
|
+
btn_run_all.clicked.connect(self.run_all_steps_3d)
|
|
711
|
+
action_layout_box.addWidget(btn_run_all)
|
|
712
|
+
action_group.setLayout(action_layout_box)
|
|
713
|
+
main_layout.addWidget(action_group)
|
|
714
|
+
|
|
715
|
+
# ========== Progress Bar ==========
|
|
716
|
+
self.progress_bar = QProgressBar()
|
|
717
|
+
self.progress_bar.setVisible(False)
|
|
718
|
+
main_layout.addWidget(self.progress_bar)
|
|
719
|
+
|
|
720
|
+
# ========== Log Output ==========
|
|
721
|
+
log_group = QGroupBox("Log Output")
|
|
722
|
+
log_layout = QVBoxLayout()
|
|
723
|
+
self.log_area = QTextEdit()
|
|
724
|
+
self.log_area.setReadOnly(True)
|
|
725
|
+
self.log_area.setStyleSheet("font-family: Monospace; font-size: 9pt;")
|
|
726
|
+
self.log_area.setMinimumHeight(350)
|
|
727
|
+
log_layout.addWidget(self.log_area)
|
|
728
|
+
log_group.setLayout(log_layout)
|
|
729
|
+
main_layout.addWidget(log_group)
|
|
730
|
+
|
|
731
|
+
# ========== Action Buttons ==========
|
|
732
|
+
action_layout = QHBoxLayout()
|
|
733
|
+
|
|
734
|
+
btn_clear_log = QPushButton("Clear Log")
|
|
735
|
+
btn_clear_log.setMaximumWidth(100)
|
|
736
|
+
btn_clear_log.clicked.connect(self.log_area.clear)
|
|
737
|
+
|
|
738
|
+
action_layout.addStretch()
|
|
739
|
+
action_layout.addWidget(btn_clear_log)
|
|
740
|
+
main_layout.addLayout(action_layout)
|
|
741
|
+
|
|
742
|
+
self.setLayout(main_layout)
|
|
743
|
+
|
|
744
|
+
def browse_data_root(self):
|
|
745
|
+
"""Open file browser to select data root directory"""
|
|
746
|
+
folder = QFileDialog.getExistingDirectory(self, "Select Data Root Directory")
|
|
747
|
+
if folder:
|
|
748
|
+
self.data_root_input.setText(folder)
|
|
749
|
+
|
|
750
|
+
def get_bbox_args(self, sensor):
|
|
751
|
+
"""Helper to build bbox arguments for a sensor"""
|
|
752
|
+
if sensor == 'lidar':
|
|
753
|
+
inputs = self.lidar_inputs
|
|
754
|
+
else: # sensor == 'camera'
|
|
755
|
+
inputs = self.camera_inputs
|
|
756
|
+
|
|
757
|
+
min_bound = [inputs['min'][ax].value() for ax in ['X', 'Y', 'Z']]
|
|
758
|
+
max_bound = [inputs['max'][ax].value() for ax in ['X', 'Y', 'Z']]
|
|
759
|
+
|
|
760
|
+
return f"--min-bound {min_bound[0]:.2f} {min_bound[1]:.2f} {min_bound[2]:.2f} --max-bound {max_bound[0]:.2f} {max_bound[1]:.2f} {max_bound[2]:.2f}"
|
|
761
|
+
|
|
762
|
+
def run_extract_board(self, sensor_type):
|
|
763
|
+
"""Run board extraction for specific sensor"""
|
|
764
|
+
data_root = self.data_root_input.text().strip()
|
|
765
|
+
|
|
766
|
+
if not data_root:
|
|
767
|
+
QMessageBox.warning(self, "Error", "Please select a data root directory")
|
|
768
|
+
return
|
|
769
|
+
|
|
770
|
+
if not os.path.exists(data_root):
|
|
771
|
+
QMessageBox.warning(self, "Error", f"Data root directory not found: {data_root}")
|
|
772
|
+
return
|
|
773
|
+
|
|
774
|
+
# Determine script args
|
|
775
|
+
bbox_args = self.get_bbox_args(sensor_type)
|
|
776
|
+
if sensor_type == "lidar":
|
|
777
|
+
args = f"--lidar {bbox_args}"
|
|
778
|
+
else: # sensor_type == "camera"
|
|
779
|
+
args = f"--camera {bbox_args}"
|
|
780
|
+
|
|
781
|
+
self.run_script("3d_board_segmentation.py", args)
|
|
782
|
+
|
|
783
|
+
def run_calibration(self):
|
|
784
|
+
"""Run calibration script"""
|
|
785
|
+
data_root = self.data_root_input.text().strip()
|
|
786
|
+
|
|
787
|
+
if not data_root:
|
|
788
|
+
QMessageBox.warning(self, "Error", "Please select a data root directory")
|
|
789
|
+
return
|
|
790
|
+
|
|
791
|
+
if not os.path.exists(data_root):
|
|
792
|
+
QMessageBox.warning(self, "Error", f"Data root directory not found: {data_root}")
|
|
793
|
+
return
|
|
794
|
+
|
|
795
|
+
method_index = self.method_combo.currentIndex()
|
|
796
|
+
if method_index == 0:
|
|
797
|
+
method = "umeyama"
|
|
798
|
+
elif method_index == 1:
|
|
799
|
+
method = "optimize"
|
|
800
|
+
else: # index == 2
|
|
801
|
+
method = "both"
|
|
802
|
+
|
|
803
|
+
script = "3d_3d_calibrator.py"
|
|
804
|
+
args = f"--method {method} --base_dir {data_root}"
|
|
805
|
+
self.run_script(script, args)
|
|
806
|
+
|
|
807
|
+
def run_all_steps_3d(self):
|
|
808
|
+
"""Run all 3D calibration steps sequentially"""
|
|
809
|
+
msg_box = QMessageBox(self)
|
|
810
|
+
msg_box.setWindowTitle("Sequential Execution")
|
|
811
|
+
msg_box.setText(
|
|
812
|
+
"Running all 3D calibration steps sequentially:\n"
|
|
813
|
+
"1. Camera Board Segmentation\n"
|
|
814
|
+
"2. LiDAR Board Segmentation\n"
|
|
815
|
+
"3. 3D-3D Calibration\n\n"
|
|
816
|
+
"This will take several minutes...")
|
|
817
|
+
msg_box.setIcon(QMessageBox.Information)
|
|
818
|
+
msg_box.setStandardButtons(QMessageBox.Ok)
|
|
819
|
+
msg_box.setStyleSheet("QMessageBox { background-color: #2B2B2B; } QLabel { color: white; } QPushButton { background-color: #4CAF50; color: white; }")
|
|
820
|
+
|
|
821
|
+
if msg_box.exec_() != QMessageBox.Ok:
|
|
822
|
+
return
|
|
823
|
+
|
|
824
|
+
# Build args using current UI values
|
|
825
|
+
cam_seg_args = self.get_bbox_args('camera')
|
|
826
|
+
lid_seg_args = self.get_bbox_args('lidar')
|
|
827
|
+
|
|
828
|
+
# Calibration Method
|
|
829
|
+
method_index = self.method_combo.currentIndex()
|
|
830
|
+
if method_index == 0: method = "umeyama"
|
|
831
|
+
elif method_index == 1: method = "optimize"
|
|
832
|
+
else: method = "both"
|
|
833
|
+
|
|
834
|
+
data_root = self.data_root_input.text().strip()
|
|
835
|
+
calib_args = f"--method {method} --base_dir {data_root}"
|
|
836
|
+
|
|
837
|
+
# Queue steps
|
|
838
|
+
self.steps_queue_3d = [
|
|
839
|
+
("3d_board_segmentation.py", f"--camera {cam_seg_args}"),
|
|
840
|
+
("3d_board_segmentation.py", f"--lidar {lid_seg_args}"),
|
|
841
|
+
("3d_3d_calibrator.py", calib_args)
|
|
842
|
+
]
|
|
843
|
+
self.current_step_index_3d = 0
|
|
844
|
+
self.run_next_step_3d()
|
|
845
|
+
|
|
846
|
+
def run_next_step_3d(self):
|
|
847
|
+
"""Run the next step in the 3D queue"""
|
|
848
|
+
if self.current_step_index_3d < len(self.steps_queue_3d):
|
|
849
|
+
script, script_args = self.steps_queue_3d[self.current_step_index_3d]
|
|
850
|
+
step_num = self.current_step_index_3d + 1
|
|
851
|
+
|
|
852
|
+
self.log_area.append(f"\n{'='*80}")
|
|
853
|
+
self.log_area.append(f"[STEP {step_num}/3] Running: {script} {script_args}")
|
|
854
|
+
self.log_area.append(f"{'='*80}\n")
|
|
855
|
+
|
|
856
|
+
self.process = QProcess()
|
|
857
|
+
self.process.readyReadStandardOutput.connect(self.handle_stdout)
|
|
858
|
+
self.process.readyReadStandardError.connect(self.handle_stderr)
|
|
859
|
+
self.process.finished.connect(self.on_step_finished_3d)
|
|
860
|
+
|
|
861
|
+
# Build and run command
|
|
862
|
+
cmd = f"python3 {script} {script_args}"
|
|
863
|
+
|
|
864
|
+
# Set working directory to src (go up from gui/widgets to src)
|
|
865
|
+
current_file = os.path.abspath(__file__)
|
|
866
|
+
src_dir = os.path.dirname(os.path.dirname(os.path.dirname(current_file)))
|
|
867
|
+
|
|
868
|
+
self.progress_bar.setVisible(True)
|
|
869
|
+
self.progress_bar.setRange(0, 0)
|
|
870
|
+
|
|
871
|
+
self.process.start("bash", ["-c", f"cd {src_dir} && {cmd}"])
|
|
872
|
+
else:
|
|
873
|
+
self.log_area.append(f"\n{'='*80}")
|
|
874
|
+
self.log_area.append("✅ All 3D calibration steps completed successfully!")
|
|
875
|
+
self.log_area.append(f"{'='*80}\n")
|
|
876
|
+
self.progress_bar.setVisible(False)
|
|
877
|
+
|
|
878
|
+
def on_step_finished_3d(self, exit_code, exit_status):
|
|
879
|
+
"""Handle completion of each step and move to next"""
|
|
880
|
+
if exit_code == 0:
|
|
881
|
+
self.log_area.append(f"✅ Step {self.current_step_index_3d + 1} completed successfully\n")
|
|
882
|
+
self.current_step_index_3d += 1
|
|
883
|
+
self.run_next_step_3d()
|
|
884
|
+
else:
|
|
885
|
+
self.log_area.append(f"❌ Step {self.current_step_index_3d + 1} failed with exit code {exit_code}\n")
|
|
886
|
+
self.progress_bar.setVisible(False)
|
|
887
|
+
msg_box = QMessageBox(self)
|
|
888
|
+
msg_box.setWindowTitle("Error")
|
|
889
|
+
msg_box.setText(f"Step {self.current_step_index_3d + 1} failed. Check logs for details.")
|
|
890
|
+
msg_box.setIcon(QMessageBox.Critical)
|
|
891
|
+
msg_box.setStyleSheet("""
|
|
892
|
+
QMessageBox {
|
|
893
|
+
background-color: #3A3A3A;
|
|
894
|
+
}
|
|
895
|
+
QMessageBox QLabel {
|
|
896
|
+
color: #FFFFFF;
|
|
897
|
+
}
|
|
898
|
+
QPushButton {
|
|
899
|
+
background-color: #f44336;
|
|
900
|
+
color: white;
|
|
901
|
+
padding: 6px 20px;
|
|
902
|
+
border-radius: 4px;
|
|
903
|
+
font-weight: bold;
|
|
904
|
+
min-width: 60px;
|
|
905
|
+
}
|
|
906
|
+
QPushButton:hover {
|
|
907
|
+
background-color: #da190b;
|
|
908
|
+
}
|
|
909
|
+
""")
|
|
910
|
+
msg_box.exec_()
|
|
911
|
+
|
|
912
|
+
def run_script(self, script_name, args="", working_dir=None):
|
|
913
|
+
"""Run a Python script with arguments"""
|
|
914
|
+
if self.process is not None and self.process.state() != QProcess.NotRunning:
|
|
915
|
+
QMessageBox.warning(self, "Warning", "A script is already running. Please wait.")
|
|
916
|
+
return
|
|
917
|
+
|
|
918
|
+
self.current_script = script_name
|
|
919
|
+
self.log_area.append(f"\n{'='*60}")
|
|
920
|
+
self.log_area.append(f"Running: {script_name} {args}")
|
|
921
|
+
self.log_area.append(f"{'='*60}\n")
|
|
922
|
+
|
|
923
|
+
self.progress_bar.setVisible(True)
|
|
924
|
+
self.progress_bar.setRange(0, 0) # Indeterminate progress
|
|
925
|
+
|
|
926
|
+
self.process = QProcess()
|
|
927
|
+
self.process.readyReadStandardOutput.connect(self.handle_stdout)
|
|
928
|
+
self.process.readyReadStandardError.connect(self.handle_stderr)
|
|
929
|
+
self.process.finished.connect(self.process_finished)
|
|
930
|
+
|
|
931
|
+
# Build command
|
|
932
|
+
cmd = f"python3 {script_name} {args}"
|
|
933
|
+
|
|
934
|
+
# Set working directory
|
|
935
|
+
if working_dir is None:
|
|
936
|
+
src_dir = os.path.dirname(os.path.abspath(__file__))
|
|
937
|
+
src_dir = os.path.dirname(os.path.dirname(src_dir)) # Go up to src/
|
|
938
|
+
else:
|
|
939
|
+
src_dir = working_dir
|
|
940
|
+
|
|
941
|
+
self.process.setWorkingDirectory(src_dir)
|
|
942
|
+
self.process.start("bash", ["-c", cmd])
|
|
943
|
+
|
|
944
|
+
def handle_stdout(self):
|
|
945
|
+
"""Handle standard output from process"""
|
|
946
|
+
data = self.process.readAllStandardOutput()
|
|
947
|
+
text = data.data().decode('utf-8', errors='replace')
|
|
948
|
+
# Append text preserving formatting
|
|
949
|
+
self.log_area.insertPlainText(text)
|
|
950
|
+
# Auto-scroll to bottom
|
|
951
|
+
self.log_area.verticalScrollBar().setValue(self.log_area.verticalScrollBar().maximum())
|
|
952
|
+
|
|
953
|
+
def handle_stderr(self):
|
|
954
|
+
"""Handle standard error from process"""
|
|
955
|
+
data = self.process.readAllStandardError()
|
|
956
|
+
text = data.data().decode('utf-8', errors='replace')
|
|
957
|
+
# Append stderr with color
|
|
958
|
+
self.log_area.insertHtml(f"<span style='color: #FF6B6B;'>{text}</span>")
|
|
959
|
+
# Auto-scroll to bottom
|
|
960
|
+
self.log_area.verticalScrollBar().setValue(self.log_area.verticalScrollBar().maximum())
|
|
961
|
+
|
|
962
|
+
def process_finished(self):
|
|
963
|
+
"""Handle process completion"""
|
|
964
|
+
self.progress_bar.setVisible(False)
|
|
965
|
+
exit_code = self.process.exitCode()
|
|
966
|
+
|
|
967
|
+
# Add completion message
|
|
968
|
+
if exit_code == 0:
|
|
969
|
+
completion_msg = f"\n{'='*60}\n✅ Process completed successfully!\n{'='*60}\n"
|
|
970
|
+
else:
|
|
971
|
+
completion_msg = f"\n{'='*60}\n❌ Process failed with exit code {exit_code}\n{'='*60}\n"
|
|
972
|
+
|
|
973
|
+
self.log_area.insertPlainText(completion_msg)
|
|
974
|
+
# Auto-scroll to bottom
|
|
975
|
+
self.log_area.verticalScrollBar().setValue(self.log_area.verticalScrollBar().maximum())
|
|
976
|
+
self.process = None
|
|
977
|
+
|