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,614 @@
1
+ from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
2
+ QPushButton, QLineEdit, QFileDialog, QMessageBox, QGroupBox,
3
+ QScrollArea, QComboBox, QSpinBox, QTextEdit, QSplitter)
4
+ from PyQt5.QtGui import QPixmap
5
+ from PyQt5.QtCore import QProcess, Qt, QSize
6
+ import os
7
+ import sys
8
+ import glob
9
+
10
+ class ValidatorWidget(QWidget):
11
+ def __init__(self):
12
+ super().__init__()
13
+ self.process = None
14
+ self.current_visualization = None
15
+ self.initUI()
16
+
17
+ def initUI(self):
18
+ main_layout = QVBoxLayout()
19
+
20
+ # ========== Configuration Section ==========
21
+ config_group = QGroupBox("Visualization Configuration")
22
+ config_layout = QVBoxLayout()
23
+
24
+ # Data Root Directory
25
+ data_root_layout = QHBoxLayout()
26
+ self.data_root_input = QLineEdit()
27
+ cwd = os.getcwd()
28
+ default_root = os.path.join(cwd, "calib_data/new_captures")
29
+ self.data_root_input.setText(default_root)
30
+ self.data_root_input.setPlaceholderText("Path to calibration data (e.g., calib_data/new_captures)")
31
+
32
+ btn_browse = QPushButton("Browse...")
33
+ btn_browse.setMaximumWidth(100)
34
+ btn_browse.clicked.connect(self.browse_data_root)
35
+
36
+ data_root_layout.addWidget(QLabel("Data Root:"))
37
+ data_root_layout.addWidget(self.data_root_input)
38
+ data_root_layout.addWidget(btn_browse)
39
+ config_layout.addLayout(data_root_layout)
40
+
41
+ config_group.setLayout(config_layout)
42
+ main_layout.addWidget(config_group)
43
+
44
+ # ========== Generate Visualization Button ==========
45
+ gen_layout = QHBoxLayout()
46
+ self.btn_generate = QPushButton("Generate Alignment Visualization")
47
+ self.btn_generate.setStyleSheet("""
48
+ QPushButton { background-color: #FFA726; color: white; padding: 10px; font-weight: bold; border-radius: 4px; }
49
+ QPushButton:hover { background-color: #FB8C00; }
50
+ QPushButton:disabled { background-color: #BDBDBD; }
51
+ """)
52
+ self.btn_generate.clicked.connect(self.generate_visualization)
53
+ gen_layout.addWidget(self.btn_generate)
54
+ gen_layout.addStretch()
55
+ main_layout.addLayout(gen_layout)
56
+
57
+ # ========== Split Layout (Log on Left, Visualization on Right) ==========
58
+ splitter = QSplitter(Qt.Horizontal)
59
+
60
+ # LEFT SIDE: Log Output
61
+ log_group = QGroupBox("Log Output")
62
+ log_layout = QVBoxLayout()
63
+ self.log_area = QTextEdit()
64
+ self.log_area.setReadOnly(True)
65
+ self.log_area.setStyleSheet("font-family: Monospace; font-size: 9pt;")
66
+ log_layout.addWidget(self.log_area)
67
+ log_group.setLayout(log_layout)
68
+
69
+ # RIGHT SIDE: Visualization
70
+ viz_group = QGroupBox("Visualization")
71
+ viz_layout = QVBoxLayout()
72
+
73
+ # Pose Selector
74
+ pose_select_layout = QHBoxLayout()
75
+ pose_label = QLabel("Select Pose:")
76
+ pose_label.setStyleSheet("color: #E0E0E0;")
77
+ pose_select_layout.addWidget(pose_label)
78
+ self.combo_pose = QComboBox()
79
+ self.combo_pose.setStyleSheet("""
80
+ QComboBox {
81
+ background-color: #3C3C3C;
82
+ color: #E0E0E0;
83
+ border: 1px solid #424242;
84
+ padding: 5px;
85
+ border-radius: 4px;
86
+ }
87
+ QComboBox::drop-down { border: none; }
88
+ QComboBox::down-arrow { color: #E0E0E0; }
89
+ QComboBox QAbstractItemView {
90
+ background-color: #3C3C3C;
91
+ color: #E0E0E0;
92
+ selection-background-color: #FFA726;
93
+ }
94
+ """)
95
+ self.combo_pose.currentIndexChanged.connect(self.on_pose_selected)
96
+ pose_select_layout.addWidget(self.combo_pose)
97
+ pose_select_layout.addStretch()
98
+ viz_layout.addLayout(pose_select_layout)
99
+
100
+ # Visualization Display Area
101
+ self.image_label = QLabel()
102
+ self.image_label.setAlignment(Qt.AlignCenter)
103
+ self.image_label.setMinimumHeight(400)
104
+ self.image_label.setStyleSheet("border: 1px solid #424242; background-color: #2C2C2C;")
105
+
106
+ scroll_area = QScrollArea()
107
+ scroll_area.setWidget(self.image_label)
108
+ scroll_area.setWidgetResizable(True)
109
+ scroll_area.setStyleSheet("background-color: #2C2C2C;")
110
+ viz_layout.addWidget(scroll_area)
111
+
112
+ # Statistics Display
113
+ stats_layout = QHBoxLayout()
114
+ self.stats_label = QLabel("No visualization loaded")
115
+ self.stats_label.setStyleSheet("font-family: Monospace; font-size: 8pt; background-color: #2C2C2C; color: #E0E0E0; padding: 8px; border-radius: 4px; max-height: 80px;")
116
+ self.stats_label.setWordWrap(True)
117
+ stats_layout.addWidget(self.stats_label)
118
+ viz_layout.addLayout(stats_layout)
119
+
120
+ viz_group.setLayout(viz_layout)
121
+ viz_group.setStyleSheet("background-color: #2C2C2C; color: #E0E0E0;")
122
+
123
+ # Add both to splitter
124
+ splitter.addWidget(log_group)
125
+ splitter.addWidget(viz_group)
126
+ splitter.setSizes([500, 500]) # Equal split
127
+ splitter.setCollapsible(0, False)
128
+ splitter.setCollapsible(1, False)
129
+ splitter.setStyleSheet("background-color: #2C2C2C;")
130
+
131
+ main_layout.addWidget(splitter, 1) # Give splitter most of the space
132
+
133
+ # ========== Action Buttons ==========
134
+ action_layout = QHBoxLayout()
135
+
136
+ btn_open_folder = QPushButton("Open Visualization Folder")
137
+ btn_open_folder.clicked.connect(self.open_viz_folder)
138
+
139
+ btn_refresh = QPushButton("Refresh Pose List")
140
+ btn_refresh.clicked.connect(self.refresh_pose_list)
141
+
142
+ btn_clear_log = QPushButton("Clear Log")
143
+ btn_clear_log.setMaximumWidth(100)
144
+ btn_clear_log.clicked.connect(self.log_area.clear)
145
+
146
+ action_layout.addWidget(btn_open_folder)
147
+ action_layout.addWidget(btn_refresh)
148
+ action_layout.addStretch()
149
+ action_layout.addWidget(btn_clear_log)
150
+ main_layout.addLayout(action_layout)
151
+
152
+ self.setLayout(main_layout)
153
+
154
+ def browse_data_root(self):
155
+ """Open file browser to select data root directory"""
156
+ folder = QFileDialog.getExistingDirectory(self, "Select Data Root Directory")
157
+ if folder:
158
+ self.data_root_input.setText(folder)
159
+ self.refresh_pose_list()
160
+
161
+ def refresh_pose_list(self):
162
+ """Refresh the list of available poses"""
163
+ data_root = self.data_root_input.text()
164
+
165
+ if not os.path.exists(data_root):
166
+ self.log_area.append(f"āš ļø Data root not found: {data_root}")
167
+ return
168
+
169
+ # Find all pose directories
170
+ pose_dirs = sorted(glob.glob(os.path.join(data_root, "pose_*")))
171
+
172
+ self.combo_pose.blockSignals(True)
173
+ self.combo_pose.clear()
174
+
175
+ for pose_dir in pose_dirs:
176
+ pose_name = os.path.basename(pose_dir)
177
+ self.combo_pose.addItem(pose_name)
178
+
179
+ self.combo_pose.blockSignals(False)
180
+
181
+ if pose_dirs:
182
+ self.log_area.append(f"āœ… Found {len(pose_dirs)} poses")
183
+ self.combo_pose.setCurrentIndex(0)
184
+ else:
185
+ self.log_area.append(f"āš ļø No pose directories found in {data_root}")
186
+
187
+ def generate_visualization(self):
188
+ """Generate alignment visualization using 2d_3d_overlay.py"""
189
+ data_root = self.data_root_input.text()
190
+
191
+ if not data_root or not os.path.exists(data_root):
192
+ QMessageBox.warning(self, "Error", f"Invalid data root: {data_root}")
193
+ return
194
+
195
+ self.btn_generate.setEnabled(False)
196
+ self.log_area.clear()
197
+ self.log_area.append(f"šŸš€ Generating visualization for: {data_root}")
198
+ self.log_area.append(f"This may take a minute...\n")
199
+
200
+ self.process = QProcess()
201
+ self.process.readyReadStandardOutput.connect(self.handle_stdout)
202
+ self.process.readyReadStandardError.connect(self.handle_stderr)
203
+ self.process.finished.connect(self.on_visualization_finished)
204
+
205
+ # Get 2d_3d_overlay.py path
206
+ script_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../2d_3d_overlay.py"))
207
+
208
+ if not os.path.exists(script_path):
209
+ self.log_area.append(f"āŒ Script not found: {script_path}")
210
+ self.btn_generate.setEnabled(True)
211
+ return
212
+
213
+ cmd = sys.executable
214
+ args = [script_path, "--data_root", data_root]
215
+
216
+ self.process.start(cmd, args)
217
+
218
+ def on_pose_selected(self, index):
219
+ """Load and display the selected pose's visualization"""
220
+ data_root = self.data_root_input.text()
221
+ pose_name = self.combo_pose.currentText()
222
+
223
+ if not pose_name:
224
+ return
225
+
226
+ viz_path = os.path.join(data_root, "alignment_viz", f"{pose_name}_viz.jpg")
227
+
228
+ if os.path.exists(viz_path):
229
+ self.display_visualization(viz_path, pose_name)
230
+ else:
231
+ self.image_label.setText(f"Visualization not found for {pose_name}\nRun 'Generate Alignment Visualization' first")
232
+ self.stats_label.setText("No visualization loaded")
233
+
234
+ def display_visualization(self, image_path, pose_name):
235
+ """Display an image in the visualization area"""
236
+ pixmap = QPixmap(image_path)
237
+
238
+ if pixmap.isNull():
239
+ self.image_label.setText(f"Failed to load image: {image_path}")
240
+ return
241
+
242
+ # Scale to fit in view while maintaining aspect ratio
243
+ scaled_pixmap = pixmap.scaledToWidth(1400, Qt.SmoothTransformation)
244
+ self.image_label.setPixmap(scaled_pixmap)
245
+
246
+ # Update statistics
247
+ self.stats_label.setText(
248
+ f"Pose: {pose_name}\n"
249
+ f"Image: {image_path}\n"
250
+ f"Size: {pixmap.width()}x{pixmap.height()}px\n"
251
+ f"\nVisualization shows:\n"
252
+ f"🟢 Camera corners (green circles)\n"
253
+ f"šŸ”“ LiDAR corners (red crosses)\n"
254
+ f"🟔 Error circles (size = error magnitude)\n"
255
+ f"Red line: Centroid offset between sensors"
256
+ )
257
+
258
+ def handle_stdout(self):
259
+ """Handle stdout from visualization process"""
260
+ data = self.process.readAllStandardOutput()
261
+ text = data.data().decode()
262
+ self.log_area.append(text)
263
+
264
+ def handle_stderr(self):
265
+ """Handle stderr from visualization process"""
266
+ data = self.process.readAllStandardError()
267
+ text = data.data().decode()
268
+ self.log_area.append(f"<span style='color:red'>{text}</span>")
269
+
270
+ def on_visualization_finished(self):
271
+ """Handle visualization generation completion"""
272
+ exit_code = self.process.exitCode()
273
+
274
+ if exit_code == 0:
275
+ self.log_area.append(f"\nāœ… Visualization generated successfully!")
276
+ self.refresh_pose_list()
277
+ else:
278
+ self.log_area.append(f"\nāŒ Visualization failed with exit code {exit_code}")
279
+
280
+ self.btn_generate.setEnabled(True)
281
+
282
+ def open_viz_folder(self):
283
+ """Open visualization folder in file explorer"""
284
+ data_root = self.data_root_input.text()
285
+ viz_folder = os.path.join(data_root, "alignment_viz")
286
+
287
+ if not os.path.exists(viz_folder):
288
+ QMessageBox.warning(self, "Not Found", f"Visualization folder not found:\n{viz_folder}")
289
+ return
290
+
291
+ # Open folder based on OS
292
+ if sys.platform == "win32":
293
+ os.startfile(viz_folder)
294
+ elif sys.platform == "darwin": # macOS
295
+ os.system(f"open '{viz_folder}'")
296
+ else: # Linux
297
+ os.system(f"xdg-open '{viz_folder}'")
298
+
299
+
300
+ class ThreeDValidatorWidget(QWidget):
301
+ """3D-3D Calibration Validation Widget - Point Cloud Overlay Visualization"""
302
+
303
+ def __init__(self):
304
+ super().__init__()
305
+ self.process = None
306
+ self.current_visualization = None
307
+ self.initUI()
308
+
309
+ def initUI(self):
310
+ main_layout = QVBoxLayout()
311
+
312
+ # ========== Configuration Section ==========
313
+ config_group = QGroupBox("3D-3D Validation Configuration")
314
+ config_layout = QVBoxLayout()
315
+
316
+ # Data Root Directory
317
+ data_root_layout = QHBoxLayout()
318
+ self.data_root_input = QLineEdit()
319
+ cwd = os.getcwd()
320
+ default_root = os.path.join(cwd, "calib_data/cb_data")
321
+ self.data_root_input.setText(default_root)
322
+ self.data_root_input.setPlaceholderText("Path to calibration data (e.g., calib_data/cb_data)")
323
+
324
+ btn_browse = QPushButton("Browse...")
325
+ btn_browse.setMaximumWidth(100)
326
+ btn_browse.clicked.connect(self.browse_data_root)
327
+
328
+ data_root_layout.addWidget(QLabel("Data Root:"))
329
+ data_root_layout.addWidget(self.data_root_input)
330
+ data_root_layout.addWidget(btn_browse)
331
+ config_layout.addLayout(data_root_layout)
332
+
333
+ # Calibration File
334
+ calib_layout = QHBoxLayout()
335
+ self.calib_input = QLineEdit()
336
+ self.calib_input.setText("final_extrinsic.yaml")
337
+ self.calib_input.setPlaceholderText("Calibration YAML file")
338
+
339
+ btn_calib_browse = QPushButton("Browse...")
340
+ btn_calib_browse.setMaximumWidth(100)
341
+ btn_calib_browse.clicked.connect(self.browse_calib_file)
342
+
343
+ calib_layout.addWidget(QLabel("Calibration File:"))
344
+ calib_layout.addWidget(self.calib_input)
345
+ calib_layout.addWidget(btn_calib_browse)
346
+ config_layout.addLayout(calib_layout)
347
+
348
+ config_group.setLayout(config_layout)
349
+ main_layout.addWidget(config_group)
350
+
351
+ # ========== Generate Visualization Button ==========
352
+ gen_layout = QHBoxLayout()
353
+ self.btn_generate = QPushButton("Generate 3D Overlay Visualization")
354
+ self.btn_generate.setStyleSheet("""
355
+ QPushButton { background-color: #2196F3; color: white; padding: 10px; font-weight: bold; border-radius: 4px; }
356
+ QPushButton:hover { background-color: #1976D2; }
357
+ QPushButton:disabled { background-color: #BDBDBD; }
358
+ """)
359
+ self.btn_generate.clicked.connect(self.generate_visualization)
360
+ gen_layout.addWidget(self.btn_generate)
361
+ gen_layout.addStretch()
362
+ main_layout.addLayout(gen_layout)
363
+
364
+ # ========== Split Layout (Log on Left, Visualization on Right) ==========
365
+ splitter = QSplitter(Qt.Horizontal)
366
+
367
+ # LEFT SIDE: Log Output
368
+ log_group = QGroupBox("Log Output")
369
+ log_layout = QVBoxLayout()
370
+ self.log_area = QTextEdit()
371
+ self.log_area.setReadOnly(True)
372
+ self.log_area.setStyleSheet("font-family: Monospace; font-size: 9pt;")
373
+ log_layout.addWidget(self.log_area)
374
+ log_group.setLayout(log_layout)
375
+
376
+ # RIGHT SIDE: Visualization
377
+ viz_group = QGroupBox("3D Point Cloud Overlay")
378
+ viz_layout = QVBoxLayout()
379
+
380
+ # Pose Selector
381
+ pose_select_layout = QHBoxLayout()
382
+ pose_label = QLabel("Select Pose:")
383
+ pose_label.setStyleSheet("color: #E0E0E0;")
384
+ pose_select_layout.addWidget(pose_label)
385
+ self.combo_pose = QComboBox()
386
+ self.combo_pose.setStyleSheet("""
387
+ QComboBox {
388
+ background-color: #3C3C3C;
389
+ color: #E0E0E0;
390
+ border: 1px solid #424242;
391
+ padding: 5px;
392
+ border-radius: 4px;
393
+ }
394
+ QComboBox::drop-down { border: none; }
395
+ QComboBox::down-arrow { color: #E0E0E0; }
396
+ QComboBox QAbstractItemView {
397
+ background-color: #3C3C3C;
398
+ color: #E0E0E0;
399
+ selection-background-color: #2196F3;
400
+ }
401
+ """)
402
+ self.combo_pose.currentIndexChanged.connect(self.on_pose_selected)
403
+ pose_select_layout.addWidget(self.combo_pose)
404
+ pose_select_layout.addStretch()
405
+ viz_layout.addLayout(pose_select_layout)
406
+
407
+ # Visualization Display Area
408
+ self.image_label = QLabel()
409
+ self.image_label.setAlignment(Qt.AlignCenter)
410
+ self.image_label.setMinimumHeight(400)
411
+ self.image_label.setStyleSheet("border: 1px solid #424242; background-color: #2C2C2C;")
412
+
413
+ scroll_area = QScrollArea()
414
+ scroll_area.setWidget(self.image_label)
415
+ scroll_area.setWidgetResizable(True)
416
+ scroll_area.setStyleSheet("background-color: #2C2C2C;")
417
+ viz_layout.addWidget(scroll_area)
418
+
419
+ # Statistics Display
420
+ stats_layout = QHBoxLayout()
421
+ self.stats_label = QLabel("No visualization loaded")
422
+ self.stats_label.setStyleSheet("font-family: Monospace; font-size: 8pt; background-color: #2C2C2C; color: #E0E0E0; padding: 8px; border-radius: 4px; max-height: 100px;")
423
+ self.stats_label.setWordWrap(True)
424
+ stats_layout.addWidget(self.stats_label)
425
+ viz_layout.addLayout(stats_layout)
426
+
427
+ viz_group.setLayout(viz_layout)
428
+ viz_group.setStyleSheet("background-color: #2C2C2C; color: #E0E0E0;")
429
+
430
+ # Add both to splitter
431
+ splitter.addWidget(log_group)
432
+ splitter.addWidget(viz_group)
433
+ splitter.setSizes([500, 500]) # Equal split
434
+ splitter.setCollapsible(0, False)
435
+ splitter.setCollapsible(1, False)
436
+ splitter.setStyleSheet("background-color: #2C2C2C;")
437
+
438
+ main_layout.addWidget(splitter, 1) # Give splitter most of the space
439
+
440
+ # ========== Action Buttons ==========
441
+ action_layout = QHBoxLayout()
442
+
443
+ btn_open_folder = QPushButton("Open Visualization Folder")
444
+ btn_open_folder.clicked.connect(self.open_viz_folder)
445
+
446
+ btn_refresh = QPushButton("Refresh Pose List")
447
+ btn_refresh.clicked.connect(self.refresh_pose_list)
448
+
449
+ btn_clear_log = QPushButton("Clear Log")
450
+ btn_clear_log.setMaximumWidth(100)
451
+ btn_clear_log.clicked.connect(self.log_area.clear)
452
+
453
+ action_layout.addWidget(btn_open_folder)
454
+ action_layout.addWidget(btn_refresh)
455
+ action_layout.addStretch()
456
+ action_layout.addWidget(btn_clear_log)
457
+ main_layout.addLayout(action_layout)
458
+
459
+ self.setLayout(main_layout)
460
+
461
+ def browse_data_root(self):
462
+ """Open file browser to select data root directory"""
463
+ folder = QFileDialog.getExistingDirectory(self, "Select Data Root Directory")
464
+ if folder:
465
+ self.data_root_input.setText(folder)
466
+ self.refresh_pose_list()
467
+
468
+ def browse_calib_file(self):
469
+ """Open file browser to select calibration file"""
470
+ file_path, _ = QFileDialog.getOpenFileName(
471
+ self, "Select Calibration File", "", "YAML Files (*.yaml *.yml);;All Files (*)"
472
+ )
473
+ if file_path:
474
+ self.calib_input.setText(file_path)
475
+
476
+ def refresh_pose_list(self):
477
+ """Refresh the list of available poses"""
478
+ data_root = self.data_root_input.text()
479
+
480
+ if not os.path.exists(data_root):
481
+ self.log_area.append(f"āš ļø Data root not found: {data_root}")
482
+ return
483
+
484
+ # Find all pose directories
485
+ pose_dirs = sorted(glob.glob(os.path.join(data_root, "pose_*")))
486
+
487
+ self.combo_pose.blockSignals(True)
488
+ self.combo_pose.clear()
489
+
490
+ for pose_dir in pose_dirs:
491
+ pose_name = os.path.basename(pose_dir)
492
+ self.combo_pose.addItem(pose_name)
493
+
494
+ self.combo_pose.blockSignals(False)
495
+
496
+ if pose_dirs:
497
+ self.log_area.append(f"āœ… Found {len(pose_dirs)} poses")
498
+ self.combo_pose.setCurrentIndex(0)
499
+ else:
500
+ self.log_area.append(f"āš ļø No pose directories found in {data_root}")
501
+
502
+ def generate_visualization(self):
503
+ """Generate 3D overlay visualization using 3d_3d_overlay.py"""
504
+ data_root = self.data_root_input.text()
505
+ calib_file = self.calib_input.text()
506
+
507
+ if not data_root or not os.path.exists(data_root):
508
+ QMessageBox.warning(self, "Error", f"Invalid data root: {data_root}")
509
+ return
510
+
511
+ self.btn_generate.setEnabled(False)
512
+ self.log_area.clear()
513
+ self.log_area.append(f"šŸš€ Generating 3D overlays for: {data_root}")
514
+ self.log_area.append(f"Using calibration: {calib_file}")
515
+ self.log_area.append(f"This may take a few minutes...\n")
516
+
517
+ self.process = QProcess()
518
+ self.process.readyReadStandardOutput.connect(self.handle_stdout)
519
+ self.process.readyReadStandardError.connect(self.handle_stderr)
520
+ self.process.finished.connect(self.on_visualization_finished)
521
+
522
+ # Get 3d_3d_overlay.py path
523
+ script_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../3d_3d_overlay.py"))
524
+
525
+ if not os.path.exists(script_path):
526
+ self.log_area.append(f"āŒ Script not found: {script_path}")
527
+ self.btn_generate.setEnabled(True)
528
+ return
529
+
530
+ cmd = sys.executable
531
+ args = [script_path, "--data_root", data_root, "--calib", calib_file]
532
+
533
+ self.process.start(cmd, args)
534
+
535
+ def on_pose_selected(self, index):
536
+ """Load and display the selected pose's visualization"""
537
+ data_root = self.data_root_input.text()
538
+ pose_name = self.combo_pose.currentText()
539
+
540
+ if not pose_name:
541
+ return
542
+
543
+ viz_path = os.path.join(data_root, pose_name, "3d_overlay_viz.png")
544
+
545
+ if os.path.exists(viz_path):
546
+ self.display_visualization(viz_path, pose_name)
547
+ else:
548
+ self.image_label.setText(f"Visualization not found for {pose_name}\nRun 'Generate 3D Overlay Visualization' first")
549
+ self.stats_label.setText("No visualization loaded")
550
+
551
+ def display_visualization(self, image_path, pose_name):
552
+ """Display an image in the visualization area"""
553
+ pixmap = QPixmap(image_path)
554
+
555
+ if pixmap.isNull():
556
+ self.image_label.setText(f"Failed to load image: {image_path}")
557
+ return
558
+
559
+ # Scale to fit in view while maintaining aspect ratio
560
+ scaled_pixmap = pixmap.scaledToWidth(1400, Qt.SmoothTransformation)
561
+ self.image_label.setPixmap(scaled_pixmap)
562
+
563
+ # Update statistics
564
+ self.stats_label.setText(
565
+ f"Pose: {pose_name}\n"
566
+ f"Image: {image_path}\n"
567
+ f"Size: {pixmap.width()}x{pixmap.height()}px\n"
568
+ f"\nVisualization shows:\n"
569
+ f"🟢 Green Points: LiDAR board points (projected to camera frame)\n"
570
+ f"šŸ”“ Red Box: Camera/depth board points (ground truth)\n"
571
+ f"🟔 Green Box: LiDAR board bounding box\n"
572
+ f"šŸ“ Error: Centroid offset in pixels | IoU: Intersection over Union of bounding boxes"
573
+ )
574
+
575
+ def handle_stdout(self):
576
+ """Handle stdout from visualization process"""
577
+ data = self.process.readAllStandardOutput()
578
+ text = data.data().decode()
579
+ self.log_area.append(text)
580
+
581
+ def handle_stderr(self):
582
+ """Handle stderr from visualization process"""
583
+ data = self.process.readAllStandardError()
584
+ text = data.data().decode()
585
+ self.log_area.append(f"<span style='color:red'>{text}</span>")
586
+
587
+ def on_visualization_finished(self):
588
+ """Handle visualization generation completion"""
589
+ exit_code = self.process.exitCode()
590
+
591
+ if exit_code == 0:
592
+ self.log_area.append(f"\nāœ… 3D overlays generated successfully!")
593
+ self.refresh_pose_list()
594
+ else:
595
+ self.log_area.append(f"\nāŒ Visualization failed with exit code {exit_code}")
596
+
597
+ self.btn_generate.setEnabled(True)
598
+
599
+ def open_viz_folder(self):
600
+ """Open visualization folder in file explorer"""
601
+ data_root = self.data_root_input.text()
602
+
603
+ if not os.path.exists(data_root):
604
+ QMessageBox.warning(self, "Not Found", f"Data root not found:\n{data_root}")
605
+ return
606
+
607
+ # Open folder based on OS
608
+ if sys.platform == "win32":
609
+ os.startfile(data_root)
610
+ elif sys.platform == "darwin": # macOS
611
+ os.system(f"open '{data_root}'")
612
+ else: # Linux
613
+ os.system(f"xdg-open '{data_root}'")
614
+
@@ -0,0 +1,56 @@
1
+ from PyQt5.QtWidgets import QMainWindow, QStackedWidget, QLabel, QWidget, QVBoxLayout, QPushButton, QHBoxLayout
2
+ from PyQt5.QtCore import Qt
3
+ from .main_window_ui import setup_styles
4
+
5
+ # Import our new widgets
6
+ try:
7
+ from ..widgets.home_widget import HomeWidget
8
+ from ..widgets.single_lidar_widget import SingleLidarWidget
9
+ from ..widgets.three_d_calib_widget import ThreeDThreeDWidget
10
+ from ..widgets.two_d_calib_widget import TwoDThreeDWidget
11
+ except ImportError:
12
+ from gui.widgets.home_widget import HomeWidget
13
+ from gui.widgets.single_lidar_widget import SingleLidarWidget
14
+ from gui.widgets.three_d_calib_widget import ThreeDThreeDWidget
15
+ from gui.widgets.two_d_calib_widget import TwoDThreeDWidget
16
+
17
+ class MainWindow(QMainWindow):
18
+ def __init__(self):
19
+ super().__init__()
20
+ self.setWindowTitle("VIRYA Calibration Suite")
21
+ self.resize(1200, 800)
22
+
23
+ setup_styles(self)
24
+
25
+ self.stack = QStackedWidget()
26
+ self.setCentralWidget(self.stack)
27
+
28
+ self.init_ui()
29
+
30
+ def init_ui(self):
31
+ # 0: Home
32
+ self.home_widget = HomeWidget()
33
+ self.home_widget.navigate.connect(self.handle_navigation)
34
+ self.stack.addWidget(self.home_widget)
35
+
36
+ # 1: Single LiDAR
37
+ self.single_lidar_widget = SingleLidarWidget()
38
+ self.single_lidar_widget.go_home.connect(self.go_home)
39
+ self.stack.addWidget(self.single_lidar_widget)
40
+
41
+ # 2: 3D-3D
42
+ self.three_d_widget = ThreeDThreeDWidget()
43
+ self.three_d_widget.go_home.connect(self.go_home)
44
+ self.stack.addWidget(self.three_d_widget)
45
+
46
+ # 3: 2D-3D
47
+ self.two_d_widget = TwoDThreeDWidget()
48
+ self.two_d_widget.go_home.connect(self.go_home)
49
+ self.stack.addWidget(self.two_d_widget)
50
+
51
+ def handle_navigation(self, index):
52
+ # index matches the stack index we assume (1, 2, 3)
53
+ self.stack.setCurrentIndex(index)
54
+
55
+ def go_home(self):
56
+ self.stack.setCurrentIndex(0)