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.
Files changed (47) hide show
  1. calibrate_suite-0.1.0.dist-info/METADATA +761 -0
  2. calibrate_suite-0.1.0.dist-info/RECORD +47 -0
  3. calibrate_suite-0.1.0.dist-info/WHEEL +5 -0
  4. calibrate_suite-0.1.0.dist-info/entry_points.txt +3 -0
  5. calibrate_suite-0.1.0.dist-info/licenses/LICENSE +201 -0
  6. calibrate_suite-0.1.0.dist-info/top_level.txt +4 -0
  7. fleet_server/__init__.py +32 -0
  8. fleet_server/app.py +377 -0
  9. fleet_server/config.py +91 -0
  10. fleet_server/templates/error.html +57 -0
  11. fleet_server/templates/index.html +137 -0
  12. fleet_server/templates/viewer.html +490 -0
  13. fleet_server/utils.py +178 -0
  14. gui/__init__.py +2 -0
  15. gui/assets/2d-or-3d-fleet-upload.png +0 -0
  16. gui/assets/2d_3d_overlay_output.jpg +0 -0
  17. gui/assets/3d-or-2d-overlay_page.png +0 -0
  18. gui/assets/3d-or-2d-record-page.png +0 -0
  19. gui/assets/3d_3d_overlay_output.png +0 -0
  20. gui/assets/3d_or_2d_calibrate-page.png +0 -0
  21. gui/assets/GUI_homepage.png +0 -0
  22. gui/assets/hardware_setup.jpeg +0 -0
  23. gui/assets/single_lidar_calibrate_page.png +0 -0
  24. gui/assets/single_lidar_output.png +0 -0
  25. gui/assets/single_lidar_record_page.png +0 -0
  26. gui/assets/virya.jpg +0 -0
  27. gui/main.py +23 -0
  28. gui/widgets/calibrator_widget.py +977 -0
  29. gui/widgets/extractor_widget.py +561 -0
  30. gui/widgets/home_widget.py +117 -0
  31. gui/widgets/recorder_widget.py +127 -0
  32. gui/widgets/single_lidar_widget.py +673 -0
  33. gui/widgets/three_d_calib_widget.py +87 -0
  34. gui/widgets/two_d_calib_widget.py +86 -0
  35. gui/widgets/uploader_widget.py +151 -0
  36. gui/widgets/validator_widget.py +614 -0
  37. gui/windows/main_window.py +56 -0
  38. gui/windows/main_window_ui.py +65 -0
  39. rviz_configs/2D-3D.rviz +183 -0
  40. rviz_configs/3D-3D.rviz +184 -0
  41. rviz_configs/default_calib.rviz +167 -0
  42. utils/__init__.py +13 -0
  43. utils/calibration_common.py +23 -0
  44. utils/cli_calibrate.py +53 -0
  45. utils/cli_fleet_server.py +64 -0
  46. utils/data_extractor_common.py +87 -0
  47. 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
+