calibrate-suite 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- calibrate_suite-0.1.0.dist-info/METADATA +761 -0
- calibrate_suite-0.1.0.dist-info/RECORD +47 -0
- calibrate_suite-0.1.0.dist-info/WHEEL +5 -0
- calibrate_suite-0.1.0.dist-info/entry_points.txt +3 -0
- calibrate_suite-0.1.0.dist-info/licenses/LICENSE +201 -0
- calibrate_suite-0.1.0.dist-info/top_level.txt +4 -0
- fleet_server/__init__.py +32 -0
- fleet_server/app.py +377 -0
- fleet_server/config.py +91 -0
- fleet_server/templates/error.html +57 -0
- fleet_server/templates/index.html +137 -0
- fleet_server/templates/viewer.html +490 -0
- fleet_server/utils.py +178 -0
- gui/__init__.py +2 -0
- gui/assets/2d-or-3d-fleet-upload.png +0 -0
- gui/assets/2d_3d_overlay_output.jpg +0 -0
- gui/assets/3d-or-2d-overlay_page.png +0 -0
- gui/assets/3d-or-2d-record-page.png +0 -0
- gui/assets/3d_3d_overlay_output.png +0 -0
- gui/assets/3d_or_2d_calibrate-page.png +0 -0
- gui/assets/GUI_homepage.png +0 -0
- gui/assets/hardware_setup.jpeg +0 -0
- gui/assets/single_lidar_calibrate_page.png +0 -0
- gui/assets/single_lidar_output.png +0 -0
- gui/assets/single_lidar_record_page.png +0 -0
- gui/assets/virya.jpg +0 -0
- gui/main.py +23 -0
- gui/widgets/calibrator_widget.py +977 -0
- gui/widgets/extractor_widget.py +561 -0
- gui/widgets/home_widget.py +117 -0
- gui/widgets/recorder_widget.py +127 -0
- gui/widgets/single_lidar_widget.py +673 -0
- gui/widgets/three_d_calib_widget.py +87 -0
- gui/widgets/two_d_calib_widget.py +86 -0
- gui/widgets/uploader_widget.py +151 -0
- gui/widgets/validator_widget.py +614 -0
- gui/windows/main_window.py +56 -0
- gui/windows/main_window_ui.py +65 -0
- rviz_configs/2D-3D.rviz +183 -0
- rviz_configs/3D-3D.rviz +184 -0
- rviz_configs/default_calib.rviz +167 -0
- utils/__init__.py +13 -0
- utils/calibration_common.py +23 -0
- utils/cli_calibrate.py +53 -0
- utils/cli_fleet_server.py +64 -0
- utils/data_extractor_common.py +87 -0
- utils/gui_helpers.py +25 -0
|
@@ -0,0 +1,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)
|