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,561 @@
1
+ from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
2
+ QPushButton, QGroupBox, QLineEdit, QComboBox, QTextEdit,
3
+ QRadioButton, QCheckBox, QFileDialog, QMessageBox)
4
+ from PyQt5.QtCore import pyqtSignal, Qt, QProcess
5
+ import os
6
+ import glob
7
+ import json
8
+ import subprocess
9
+
10
+ class ExtractorWidget(QWidget):
11
+ """
12
+ Recorder Widget adapted for 3D-3D (LiDAR-Camera) Calibration.
13
+ (Formerly MultimodalRecorderWidget)
14
+ """
15
+ back_clicked = pyqtSignal()
16
+
17
+ def __init__(self, enable_depth=True, script_name="3d_3d_data_extractor.py"):
18
+ super().__init__()
19
+ self.enable_depth = enable_depth
20
+ self.script_name = script_name
21
+ self.record_process = None
22
+ self.diag_process = None
23
+ self.initUI()
24
+
25
+ def initUI(self):
26
+ # Dark Theme Stylesheet
27
+ self.setStyleSheet("""
28
+ QLineEdit, QComboBox, QListWidget {
29
+ background-color: #404040;
30
+ color: #ffffff;
31
+ border: 1px solid #666;
32
+ padding: 5px;
33
+ }
34
+ QLineEdit:disabled, QComboBox:disabled, QListWidget:disabled {
35
+ background-color: #2a2a2a;
36
+ color: #888;
37
+ }
38
+ QMessageBox {
39
+ background-color: #2b2b2b;
40
+ color: #ffffff;
41
+ }
42
+ QMessageBox QLabel {
43
+ color: #ffffff;
44
+ }
45
+ QGroupBox {
46
+ font-weight: bold;
47
+ border: 1px solid #555;
48
+ border-radius: 5px;
49
+ margin-top: 7px;
50
+ padding-top: 5px;
51
+ }
52
+ QGroupBox::title {
53
+ subcontrol-origin: margin;
54
+ left: 10px;
55
+ padding: 0 3px 0 3px;
56
+ }
57
+ QComboBox QAbstractItemView {
58
+ background-color: #404040;
59
+ color: #ffffff;
60
+ selection-background-color: #4CAF50;
61
+ selection-color: white;
62
+ }
63
+ /* Removed global QPushButton style */
64
+ """)
65
+
66
+ layout = QVBoxLayout()
67
+ layout.setSpacing(6)
68
+ layout.setContentsMargins(5, 5, 5, 5)
69
+
70
+ # 1. Source Type
71
+ type_group = QGroupBox("Input Type & Sync Mode")
72
+ type_layout = QHBoxLayout()
73
+ type_layout.setContentsMargins(5, 5, 5, 5)
74
+ type_layout.setSpacing(65)
75
+
76
+ self.rb_live = QRadioButton("Live Stream (ROS2)")
77
+ self.rb_bag = QRadioButton("Rosbag Recording")
78
+ self.rb_live.setChecked(True)
79
+ self.rb_live.toggled.connect(self.update_ui_state)
80
+
81
+ # Add checkbox for requiring all topics
82
+ self.chk_require_all = QCheckBox("Require All Topics")
83
+ self.chk_require_all.setToolTip("When checked, all 4 topics must be present. When unchecked, only available topics will be recorded.")
84
+ self.chk_require_all.setChecked(False) # Default to flexible mode
85
+
86
+ # Sync Mode checkbox
87
+ self.chk_strict_sync = QCheckBox("Timestamp Sync")
88
+ self.chk_strict_sync.setToolTip(
89
+ "✓ CHECKED: ApproximateTimeSynchronizer (real sensors)\n"
90
+ "✗ UNCHECKED: Latest Messages (bag replay)\n"
91
+ "Auto-fallback if clock skew detected!"
92
+ )
93
+ self.chk_strict_sync.setChecked(True) # Default: WITH timestamp
94
+
95
+ type_layout.addWidget(self.rb_live)
96
+ type_layout.addWidget(self.rb_bag)
97
+ type_layout.addSpacing(65)
98
+ type_layout.addWidget(self.chk_require_all)
99
+ type_layout.addSpacing(65)
100
+ type_layout.addWidget(self.chk_strict_sync)
101
+ type_layout.addStretch()
102
+ type_group.setLayout(type_layout)
103
+ layout.addWidget(type_group)
104
+
105
+ # 2. Configuration (Topics & Bag)
106
+ self.config_group = QGroupBox("Configuration")
107
+ conf_layout = QVBoxLayout()
108
+ conf_layout.setContentsMargins(5, 5, 5, 5)
109
+
110
+ # Live Config (Multimodal Topics)
111
+ self.live_widget = QWidget()
112
+ live_l = QVBoxLayout()
113
+ live_l.setContentsMargins(0, 0, 0, 0)
114
+
115
+ # Topic Rows
116
+ self.combo_lidar = QComboBox()
117
+ self.combo_cam = QComboBox()
118
+ self.combo_depth = QComboBox()
119
+ self.combo_info = QComboBox()
120
+
121
+ btn_refresh = QPushButton("🔄 Scan")
122
+ btn_refresh.setFixedWidth(80)
123
+ btn_refresh.clicked.connect(self.scan_topics)
124
+
125
+ # Layout for topics
126
+ row_lidar = QHBoxLayout()
127
+ row_lidar.addWidget(QLabel("LiDAR:"))
128
+ row_lidar.addWidget(self.combo_lidar, 1)
129
+ row_lidar.addWidget(btn_refresh)
130
+
131
+ row_cam = QHBoxLayout()
132
+ row_cam.addWidget(QLabel("Image:"))
133
+ row_cam.addWidget(self.combo_cam, 1)
134
+
135
+ if self.enable_depth:
136
+ row_depth = QHBoxLayout()
137
+ row_depth.addWidget(QLabel("CamDepth:"))
138
+ row_depth.addWidget(self.combo_depth, 1)
139
+
140
+ row_info = QHBoxLayout()
141
+ row_info.addWidget(QLabel("CamInfo:"))
142
+ row_info.addWidget(self.combo_info, 1)
143
+
144
+ live_l.addLayout(row_lidar)
145
+ live_l.addLayout(row_cam)
146
+ if self.enable_depth:
147
+ live_l.addLayout(row_depth)
148
+ live_l.addLayout(row_info)
149
+ self.live_widget.setLayout(live_l)
150
+ conf_layout.addWidget(self.live_widget)
151
+
152
+ # Bag Config
153
+ self.bag_widget = QWidget()
154
+ self.bag_widget.setVisible(False)
155
+ bag_l = QVBoxLayout()
156
+ bag_l.setContentsMargins(0, 0, 0, 0)
157
+
158
+ # Rosbag file selection
159
+ bag_file_l = QHBoxLayout()
160
+ self.bag_path = QLineEdit()
161
+ self.bag_path.setPlaceholderText("Path to Rosbag Folder")
162
+ btn_browse_bag = QPushButton("Browse")
163
+ btn_browse_bag.clicked.connect(self.browse_bag)
164
+ self.cb_loop = QCheckBox("Loop")
165
+ self.cb_loop.setChecked(True)
166
+ bag_file_l.addWidget(QLabel("Folder:"))
167
+ bag_file_l.addWidget(self.bag_path, 1)
168
+ bag_file_l.addWidget(btn_browse_bag)
169
+ bag_file_l.addWidget(self.cb_loop)
170
+ bag_l.addLayout(bag_file_l)
171
+
172
+ # Topic selection for rosbag
173
+ self.bag_topics_widget = QWidget()
174
+ bag_topics_l = QVBoxLayout()
175
+ bag_topics_l.setContentsMargins(0, 0, 0, 0)
176
+
177
+ # Topic Rows for bag mode
178
+ self.bag_combo_lidar = QComboBox()
179
+ self.bag_combo_cam = QComboBox()
180
+ self.bag_combo_depth = QComboBox()
181
+ self.bag_combo_info = QComboBox()
182
+
183
+ btn_scan_bag = QPushButton("🔄 Scan")
184
+ btn_scan_bag.setFixedWidth(80)
185
+ btn_scan_bag.clicked.connect(self.scan_bag_topics)
186
+
187
+ # Layout for bag topics
188
+ bag_row_lidar = QHBoxLayout()
189
+ bag_row_lidar.addWidget(QLabel("LiDAR:"))
190
+ bag_row_lidar.addWidget(self.bag_combo_lidar, 1)
191
+ bag_row_lidar.addWidget(btn_scan_bag)
192
+
193
+ bag_row_cam = QHBoxLayout()
194
+ bag_row_cam.addWidget(QLabel("Image:"))
195
+ bag_row_cam.addWidget(self.bag_combo_cam, 1)
196
+
197
+ if self.enable_depth:
198
+ bag_row_depth = QHBoxLayout()
199
+ bag_row_depth.addWidget(QLabel("CamDepth:"))
200
+ bag_row_depth.addWidget(self.bag_combo_depth, 1)
201
+
202
+ bag_row_info = QHBoxLayout()
203
+ bag_row_info.addWidget(QLabel("CamInfo:"))
204
+ bag_row_info.addWidget(self.bag_combo_info, 1)
205
+
206
+ bag_topics_l.addLayout(bag_row_lidar)
207
+ bag_topics_l.addLayout(bag_row_cam)
208
+ if self.enable_depth:
209
+ bag_topics_l.addLayout(bag_row_depth)
210
+ bag_topics_l.addLayout(bag_row_info)
211
+ self.bag_topics_widget.setLayout(bag_topics_l)
212
+ bag_l.addWidget(self.bag_topics_widget)
213
+
214
+ self.bag_widget.setLayout(bag_l)
215
+ conf_layout.addWidget(self.bag_widget)
216
+
217
+ self.config_group.setLayout(conf_layout)
218
+ layout.addWidget(self.config_group)
219
+
220
+ # 3. Output Settings
221
+ out_group = QGroupBox("Output & Visual Settings")
222
+ out_layout = QHBoxLayout()
223
+ out_layout.setContentsMargins(6, 6, 6, 6)
224
+
225
+ # Names
226
+ name_v = QVBoxLayout()
227
+ self.out_folder = QLineEdit()
228
+ self.out_folder.setPlaceholderText("Folder Name (e.g 'set1')")
229
+ self.pose_prefix = QLineEdit()
230
+ self.pose_prefix.setPlaceholderText("Prefix (e.g 'pose')")
231
+
232
+ r1 = QHBoxLayout()
233
+ r1.addWidget(QLabel("Folder:"))
234
+ r1.addWidget(self.out_folder)
235
+ name_v.addLayout(r1)
236
+
237
+ r2 = QHBoxLayout()
238
+ r2.addWidget(QLabel("Prefix:"))
239
+ r2.addWidget(self.pose_prefix)
240
+ name_v.addLayout(r2)
241
+ out_layout.addLayout(name_v, 1)
242
+
243
+ # RViz
244
+ rviz_v = QVBoxLayout()
245
+ self.rviz_combo = QComboBox()
246
+ self.scan_rviz_configs()
247
+ self.rviz_new = QLineEdit()
248
+ self.rviz_new.setPlaceholderText("New Config Name")
249
+
250
+ rr1 = QHBoxLayout()
251
+ rr1.addWidget(QLabel("RViz Config:"))
252
+ rr1.addWidget(self.rviz_combo)
253
+ rviz_v.addLayout(rr1)
254
+
255
+ rr2 = QHBoxLayout()
256
+ rr2.addWidget(QLabel("Create New:"))
257
+ rr2.addWidget(self.rviz_new)
258
+ rviz_v.addLayout(rr2)
259
+ out_layout.addLayout(rviz_v, 1)
260
+
261
+ out_group.setLayout(out_layout)
262
+ layout.addWidget(out_group)
263
+
264
+ # 4. Log
265
+ self.rec_log = QTextEdit()
266
+ self.rec_log.setReadOnly(True)
267
+ self.rec_log.setStyleSheet("background-color: #1e1e1e; color: #00ff00; font-family: Monospace;")
268
+ layout.addWidget(self.rec_log, 1)
269
+
270
+ # 6. Controls
271
+ ctrl_layout = QHBoxLayout()
272
+ self.btn_start = QPushButton("▶ Start")
273
+ self.btn_start.setStyleSheet("""
274
+ QPushButton { background-color: #4CAF50; color: white; padding: 10px; font-weight: bold; }
275
+ QPushButton:hover { background-color: #66BB6A; }
276
+ """)
277
+ self.btn_start.clicked.connect(self.start_process)
278
+
279
+ self.btn_pause = QPushButton("⏯ Play/Pause")
280
+ self.btn_pause.setStyleSheet("""
281
+ QPushButton { background-color: #2196F3; color: white; padding: 10px; }
282
+ QPushButton:hover { background-color: #42A5F5; }
283
+ """)
284
+ self.btn_pause.clicked.connect(self.toggle_pause)
285
+ self.btn_pause.setEnabled(False)
286
+
287
+ self.btn_capture = QPushButton("📸 Capture Set")
288
+ self.btn_capture.setStyleSheet("""
289
+ QPushButton { background-color: #FF9800; color: white; padding: 10px; font-weight: bold; }
290
+ QPushButton:hover { background-color: #FFA726; }
291
+ """)
292
+ self.btn_capture.clicked.connect(self.capture_frame)
293
+ self.btn_capture.setEnabled(False)
294
+
295
+ self.btn_stop = QPushButton("⏹ Stop")
296
+ self.btn_stop.setStyleSheet("""
297
+ QPushButton { background-color: #d32f2f; color: white; padding: 10px; }
298
+ QPushButton:hover { background-color: #ef5350; }
299
+ """)
300
+ self.btn_stop.clicked.connect(self.stop_process)
301
+ self.btn_stop.setEnabled(False)
302
+
303
+ ctrl_layout.addWidget(self.btn_start)
304
+ ctrl_layout.addWidget(self.btn_pause)
305
+ ctrl_layout.addWidget(self.btn_capture)
306
+ ctrl_layout.addWidget(self.btn_stop)
307
+ layout.addLayout(ctrl_layout)
308
+
309
+ # Back Btn (Consistency)
310
+ btn_back = QPushButton("Back")
311
+ btn_back.clicked.connect(self.on_back_clicked)
312
+ # Explicit style to match SingleLiDAR
313
+ btn_back.setStyleSheet("""
314
+ QPushButton { background-color: #2E8B57; border-radius: 8px; color: white; padding: 5px; }
315
+ QPushButton:hover { background-color: #689F38; }
316
+ """)
317
+ layout.addWidget(btn_back)
318
+
319
+ self.setLayout(layout)
320
+
321
+ def on_back_clicked(self):
322
+ self.stop_process()
323
+ self.back_clicked.emit()
324
+
325
+ # --- Logic Methods ---
326
+ def update_ui_state(self):
327
+ is_live = self.rb_live.isChecked()
328
+ self.live_widget.setVisible(is_live)
329
+ self.bag_widget.setVisible(not is_live)
330
+ self.btn_pause.setEnabled(False)
331
+
332
+ def scan_topics(self):
333
+ self.rec_log.append("Scanning ROS2 topics...")
334
+ self.combo_lidar.clear()
335
+ self.combo_cam.clear()
336
+ self.combo_depth.clear()
337
+ self.combo_info.clear()
338
+
339
+ script_path = os.path.abspath(os.path.join(os.path.dirname(__file__), f"../../{self.script_name}"))
340
+ proc = subprocess.run(['python3', script_path, '--mode', 'discover', '--format', 'json'],
341
+ capture_output=True, text=True)
342
+ try:
343
+ data = json.loads(proc.stdout)
344
+
345
+ # Populate from backend categorization
346
+ self.combo_lidar.addItems(sorted(data.get('lidar', [])))
347
+ self.combo_cam.addItems(sorted(data.get('image', [])))
348
+ self.combo_depth.addItems(sorted(data.get('depth', [])))
349
+ self.combo_info.addItems(sorted(data.get('cam_info', [])))
350
+
351
+ # Count real topics (exclude 'None')
352
+ total = sum(len([x for x in v if x != 'None']) for v in data.values() if isinstance(v, list))
353
+ self.rec_log.append(f"Topics scanned. Found {total} active topics.")
354
+
355
+ except json.JSONDecodeError:
356
+ self.rec_log.append("Error scanning topics.")
357
+
358
+ def scan_bag_topics(self):
359
+ bag_path = self.bag_path.text().strip()
360
+ if not bag_path or not os.path.exists(bag_path):
361
+ QMessageBox.warning(self, "Warning", "Please select a valid rosbag folder first.")
362
+ return
363
+
364
+ self.rec_log.append("Scanning rosbag topics...")
365
+ self.bag_combo_lidar.clear()
366
+ self.bag_combo_cam.clear()
367
+ self.bag_combo_depth.clear()
368
+ self.bag_combo_info.clear()
369
+
370
+ script_path = os.path.abspath(os.path.join(os.path.dirname(__file__), f"../../{self.script_name}"))
371
+ proc = subprocess.run(['python3', script_path, '--mode', 'discover_bag', '--bag', bag_path, '--format', 'json'],
372
+ capture_output=True, text=True)
373
+
374
+ self.rec_log.append(f"Script stdout: {proc.stdout}")
375
+ self.rec_log.append(f"Script stderr: {proc.stderr}")
376
+
377
+ try:
378
+ data = json.loads(proc.stdout)
379
+
380
+ # Populate from backend categorization
381
+ self.bag_combo_lidar.addItems(data.get('lidar', ['None']))
382
+ self.bag_combo_cam.addItems(data.get('image', ['None']))
383
+ self.bag_combo_depth.addItems(data.get('depth', ['None']))
384
+ self.bag_combo_info.addItems(data.get('cam_info', ['None']))
385
+
386
+ # Count real topics (exclude 'None')
387
+ total = sum(len([x for x in v if x != 'None']) for v in data.values() if isinstance(v, list))
388
+ self.rec_log.append(f"Rosbag topics scanned. Found {total} topics.")
389
+
390
+ except json.JSONDecodeError as e:
391
+ self.rec_log.append(f"JSON decode error: {e}")
392
+ self.rec_log.append(f"Raw output: {repr(proc.stdout)}")
393
+ except Exception as e:
394
+ self.rec_log.append(f"Unexpected error: {e}")
395
+
396
+ def scan_rviz_configs(self):
397
+ self.rviz_combo.clear()
398
+ config_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../rviz_configs"))
399
+ os.makedirs(config_dir, exist_ok=True)
400
+ files = glob.glob(os.path.join(config_dir, "*.rviz"))
401
+ names = [os.path.basename(f) for f in files]
402
+ if not names: self.rviz_combo.addItem("default_calib.rviz")
403
+ else: self.rviz_combo.addItems(sorted(names))
404
+
405
+ def browse_bag(self):
406
+ path = QFileDialog.getExistingDirectory(self, "Select Rosbag Folder", "")
407
+ if path: self.bag_path.setText(path)
408
+
409
+ def start_process(self):
410
+ script_path = os.path.abspath(os.path.join(os.path.dirname(__file__), f"../../{self.script_name}"))
411
+ is_live = self.rb_live.isChecked()
412
+ args = ["-u", script_path]
413
+
414
+ # Determine sync mode based on checkbox
415
+ sync_mode = 'strict' if self.chk_strict_sync.isChecked() else 'relaxed'
416
+
417
+ if is_live:
418
+ lidar = self.combo_lidar.currentText()
419
+ cam = self.combo_cam.currentText()
420
+
421
+ if self.enable_depth:
422
+ depth = self.combo_depth.currentText()
423
+ else:
424
+ depth = 'None'
425
+
426
+ info = self.combo_info.currentText()
427
+
428
+ if not lidar or not cam:
429
+ QMessageBox.warning(self, "Warning", "Select at least LiDAR and Camera topics.")
430
+ return
431
+
432
+ args.extend(['--mode', 'live',
433
+ '--lidar_topic', lidar,
434
+ '--camera_topic', cam,
435
+ '--depth_topic', depth,
436
+ '--caminfo_topic', info,
437
+ '--sync-mode', sync_mode])
438
+
439
+ # Add require_all_topics flag if checkbox is checked
440
+ if self.chk_require_all.isChecked():
441
+ args.append('--require_all_topics')
442
+
443
+ self.btn_pause.setEnabled(True) # Enabled for Master Pause
444
+ else:
445
+ bag = self.bag_path.text().strip()
446
+ if not os.path.exists(bag):
447
+ QMessageBox.warning(self, "Warning", "Invalid bag path.")
448
+ return
449
+
450
+ # Get selected topics for bag mode
451
+ bag_lidar = self.bag_combo_lidar.currentText()
452
+ bag_cam = self.bag_combo_cam.currentText()
453
+
454
+ if self.enable_depth:
455
+ bag_depth = self.bag_combo_depth.currentText()
456
+ else:
457
+ bag_depth = 'None'
458
+
459
+ bag_info = self.bag_combo_info.currentText()
460
+
461
+ args.extend(['--mode', 'bag', '--bag', bag,
462
+ '--lidar_topic', bag_lidar,
463
+ '--camera_topic', bag_cam,
464
+ '--depth_topic', bag_depth,
465
+ '--caminfo_topic', bag_info,
466
+ '--sync-mode', sync_mode])
467
+
468
+ # Add require_all_topics flag if checkbox is checked
469
+ if self.chk_require_all.isChecked():
470
+ args.append('--require_all_topics')
471
+
472
+ if not self.cb_loop.isChecked(): args.append('--play_once')
473
+ self.btn_pause.setEnabled(True)
474
+
475
+ # Naming & RViz
476
+ if self.out_folder.text(): args.extend(['--output_name', self.out_folder.text()])
477
+ if self.pose_prefix.text(): args.extend(['--pose_prefix', self.pose_prefix.text()])
478
+
479
+ new_rviz = self.rviz_new.text().strip()
480
+ sel_rviz = self.rviz_combo.currentText()
481
+ if new_rviz:
482
+ if not new_rviz.endswith('.rviz'): new_rviz += '.rviz'
483
+ args.extend(['--rviz_config', new_rviz])
484
+ elif sel_rviz:
485
+ args.extend(['--rviz_config', sel_rviz])
486
+
487
+ self.rec_log.clear()
488
+ self.rec_log.append(f"🟢 Starting Process: {'Live' if is_live else 'Bag'} Mode (Multimodal)")
489
+
490
+ self.record_process = QProcess()
491
+ self.record_process.readyReadStandardOutput.connect(self.handle_stdout)
492
+ self.record_process.readyReadStandardError.connect(self.handle_stderr)
493
+ self.record_process.finished.connect(self.on_finished)
494
+ self.record_process.start("python3", args)
495
+
496
+ self.btn_start.setEnabled(False)
497
+ self.btn_capture.setEnabled(True)
498
+ self.btn_stop.setEnabled(True)
499
+
500
+ # Disable configs
501
+ self.live_widget.setEnabled(False)
502
+ self.bag_widget.setEnabled(False)
503
+ self.out_folder.setEnabled(False)
504
+ self.pose_prefix.setEnabled(False)
505
+ self.rviz_combo.setEnabled(False)
506
+
507
+ def stop_process(self):
508
+ if self.record_process and self.record_process.state() == QProcess.Running:
509
+ self.record_process.write(b'q\n')
510
+ self.record_process.waitForFinished(1000)
511
+ if self.record_process.state() == QProcess.Running:
512
+ self.record_process.kill()
513
+
514
+ def capture_frame(self):
515
+ if self.record_process: self.record_process.write(b'n\n')
516
+
517
+ def toggle_pause(self):
518
+ if self.record_process: self.record_process.write(b'p\n')
519
+
520
+ def handle_stdout(self):
521
+ data = self.record_process.readAllStandardOutput()
522
+ self.rec_log.append(data.data().decode().strip())
523
+
524
+ def handle_stderr(self):
525
+ data = self.record_process.readAllStandardError()
526
+ text = data.data().decode().strip()
527
+ self.rec_log.append(text)
528
+
529
+ # Check for topic validation error
530
+ if "[ERROR] All topics are 'None'" in text:
531
+ QMessageBox.critical(self, "Topic Error",
532
+ "All topics are 'None'.\n\nPlease select valid topics before starting the process.")
533
+ self.stop_process()
534
+ elif "[ERROR] Some topics are 'None'" in text:
535
+ # Determine expected topic count based on script
536
+ if self.script_name == "2d_3d_data_extractor.py":
537
+ topic_count = "3"
538
+ mode_name = "2D-3D"
539
+ else:
540
+ topic_count = "4"
541
+ mode_name = "3D-3D"
542
+
543
+ QMessageBox.critical(self, "Topic Error",
544
+ f"Some topics are 'None'.\n\nWhen 'Require All Topics' is checked, all {topic_count} topics must be selected for {mode_name} calibration mode.")
545
+ self.stop_process()
546
+
547
+ def on_finished(self):
548
+ self.btn_start.setEnabled(True)
549
+ self.btn_capture.setEnabled(False)
550
+ self.btn_stop.setEnabled(False)
551
+ self.btn_pause.setEnabled(False)
552
+
553
+ self.live_widget.setEnabled(True)
554
+ self.bag_widget.setEnabled(True)
555
+ self.out_folder.setEnabled(True)
556
+ self.pose_prefix.setEnabled(True)
557
+ self.rviz_combo.setEnabled(True)
558
+ self.scan_rviz_configs()
559
+
560
+ self.rec_log.append("\n✅ Process Stopped.")
561
+
@@ -0,0 +1,117 @@
1
+ import os
2
+ from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
3
+ QPushButton, QSizePolicy, QSpacerItem, QFrame, QGraphicsDropShadowEffect)
4
+ from PyQt5.QtCore import Qt, pyqtSignal, QRect
5
+ from PyQt5.QtGui import QFont, QPixmap, QPainter, QBrush, QPalette, QColor
6
+
7
+ class HomeWidget(QWidget):
8
+ # Signals to tell MainWindow to switch keys
9
+ # 1: Single LiDAR, 2: 3D-3D, 3: 2D-3D
10
+ navigate = pyqtSignal(int)
11
+
12
+ def __init__(self):
13
+ super().__init__()
14
+ self.initUI()
15
+
16
+ def rounded_pixmap(self, path, size=200, radius=100):
17
+ # Build absolute path relative to this script
18
+ base_dir = os.path.dirname(os.path.abspath(__file__))
19
+ abs_path = os.path.join(base_dir, "..", "assets", path)
20
+ abs_path = os.path.abspath(abs_path)
21
+ pixmap = QPixmap(abs_path)
22
+ if pixmap.isNull():
23
+ print(f"Warning: Logo image not found at {abs_path}")
24
+ return QPixmap(size, size) # Return empty pixmap
25
+ pixmap = pixmap.scaled(size, size, Qt.KeepAspectRatioByExpanding, Qt.SmoothTransformation)
26
+ rounded = QPixmap(size, size)
27
+ rounded.fill(Qt.transparent)
28
+
29
+ painter = QPainter(rounded)
30
+ painter.setRenderHint(QPainter.Antialiasing)
31
+ painter.setBrush(QBrush(pixmap))
32
+ painter.setPen(Qt.NoPen)
33
+ painter.drawRoundedRect(QRect(0, 0, size, size), radius, radius)
34
+ painter.end()
35
+
36
+ return rounded
37
+
38
+ def initUI(self):
39
+ layout = QVBoxLayout()
40
+ layout.setAlignment(Qt.AlignCenter)
41
+ layout.setSpacing(40)
42
+
43
+ # 1. Company Logo & Name
44
+ title_label = QLabel("VIRYA AUTONOMOUS")
45
+ title_label.setAlignment(Qt.AlignCenter)
46
+ title_label.setStyleSheet("font-size: 40px; font-weight: bold; color: #4CAF50;")
47
+ layout.addWidget(title_label)
48
+
49
+ # Add shadow effect to title
50
+ shadow = QGraphicsDropShadowEffect()
51
+ shadow.setBlurRadius(8)
52
+ shadow.setColor(QColor(0, 0, 0, 160))
53
+ shadow.setOffset(2, 2)
54
+ title_label.setGraphicsEffect(shadow)
55
+
56
+ # Logo with rounded edges
57
+ logo_label = QLabel()
58
+ logo_label.setAlignment(Qt.AlignCenter)
59
+ logo_label.setPixmap(self.rounded_pixmap("virya.jpg", size=200, radius=100))
60
+ layout.addWidget(logo_label)
61
+
62
+ # Spacer
63
+ layout.addSpacerItem(QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding))
64
+
65
+ # 2. "Select Calibration Type"
66
+ subtitle = QLabel("Choose Calibration Mode")
67
+ subtitle.setAlignment(Qt.AlignCenter)
68
+ subtitle.setStyleSheet("font-size: 24px; color: #e0e0e0; margin-bottom: 20px;")
69
+ layout.addWidget(subtitle)
70
+
71
+ # 3. Buttons Container
72
+ btn_layout = QHBoxLayout()
73
+ btn_layout.setSpacing(30)
74
+ btn_layout.setAlignment(Qt.AlignCenter)
75
+
76
+ # Button Style
77
+ btn_style = """
78
+ QPushButton {
79
+ background-color: #32CD32;
80
+ color: white;
81
+ border: 1px solid #228B22;
82
+ border-radius: 15px;
83
+ padding: 30px;
84
+ font-size: 18px;
85
+ min-width: 180px;
86
+ }
87
+ QPushButton:hover {
88
+ background-color: #3CB371;
89
+ border-color: #228B22;
90
+ }
91
+ QPushButton:pressed {
92
+ background-color: #228B22;
93
+ }
94
+ """
95
+
96
+ self.btn_single = QPushButton("Single LiDAR\nCalibration")
97
+ self.btn_single.setStyleSheet(btn_style)
98
+ self.btn_single.clicked.connect(lambda: self.navigate.emit(1))
99
+
100
+ self.btn_3d3d = QPushButton("3D-3D\nCalibration")
101
+ self.btn_3d3d.setStyleSheet(btn_style)
102
+ self.btn_3d3d.clicked.connect(lambda: self.navigate.emit(2))
103
+
104
+ self.btn_2d3d = QPushButton("2D-3D\nCalibration")
105
+ self.btn_2d3d.setStyleSheet(btn_style)
106
+ self.btn_2d3d.clicked.connect(lambda: self.navigate.emit(3))
107
+
108
+ btn_layout.addWidget(self.btn_single)
109
+ btn_layout.addWidget(self.btn_3d3d)
110
+ btn_layout.addWidget(self.btn_2d3d)
111
+
112
+ layout.addLayout(btn_layout)
113
+
114
+ # Bottom spacer
115
+ layout.addSpacerItem(QSpacerItem(20, 100, QSizePolicy.Minimum, QSizePolicy.Expanding))
116
+
117
+ self.setLayout(layout)