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,673 @@
|
|
|
1
|
+
from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
|
|
2
|
+
QPushButton, QStackedWidget, QFileDialog,
|
|
3
|
+
QLineEdit, QSpinBox, QMessageBox, QGroupBox, QTextEdit,
|
|
4
|
+
QComboBox, QRadioButton, QButtonGroup, QCheckBox, QSizePolicy,
|
|
5
|
+
QListWidget, QListWidgetItem, QAbstractItemView, QTabWidget)
|
|
6
|
+
from PyQt5.QtCore import Qt, QThread, pyqtSignal, QProcess
|
|
7
|
+
import os
|
|
8
|
+
import json
|
|
9
|
+
import subprocess
|
|
10
|
+
import glob
|
|
11
|
+
|
|
12
|
+
# --- Widget ---
|
|
13
|
+
class SingleLidarWidget(QWidget):
|
|
14
|
+
go_home = pyqtSignal() # Signal to return to main menu
|
|
15
|
+
|
|
16
|
+
def __init__(self):
|
|
17
|
+
super().__init__()
|
|
18
|
+
self.calib_process = None
|
|
19
|
+
self.record_process = None
|
|
20
|
+
self.diag_process = None
|
|
21
|
+
self.initUI()
|
|
22
|
+
|
|
23
|
+
def initUI(self):
|
|
24
|
+
# Apply dark style to inputs for legibility
|
|
25
|
+
self.setStyleSheet("""
|
|
26
|
+
QLineEdit, QComboBox, QListWidget, QSpinBox, QDoubleSpinBox {
|
|
27
|
+
background-color: #404040;
|
|
28
|
+
color: #ffffff;
|
|
29
|
+
border: 1px solid #666;
|
|
30
|
+
padding: 5px;
|
|
31
|
+
}
|
|
32
|
+
QLineEdit:disabled, QComboBox:disabled, QListWidget:disabled {
|
|
33
|
+
background-color: #2a2a2a;
|
|
34
|
+
color: #888;
|
|
35
|
+
}
|
|
36
|
+
QMessageBox {
|
|
37
|
+
background-color: #2b2b2b;
|
|
38
|
+
color: #ffffff;
|
|
39
|
+
}
|
|
40
|
+
QMessageBox QLabel {
|
|
41
|
+
color: #ffffff;
|
|
42
|
+
}
|
|
43
|
+
QComboBox QAbstractItemView {
|
|
44
|
+
background-color: #404040;
|
|
45
|
+
color: #ffffff;
|
|
46
|
+
selection-background-color: #4CAF50;
|
|
47
|
+
selection-color: white;
|
|
48
|
+
border: 1px solid #666;
|
|
49
|
+
}
|
|
50
|
+
QListWidget::item {
|
|
51
|
+
padding: 5px;
|
|
52
|
+
}
|
|
53
|
+
QListWidget::item:selected {
|
|
54
|
+
background-color: #4CAF50;
|
|
55
|
+
color: white;
|
|
56
|
+
}
|
|
57
|
+
/* Removed global QPushButton style to allow buttons to retain default or specific colors */
|
|
58
|
+
/* Removed global QPushButton style */
|
|
59
|
+
""")
|
|
60
|
+
|
|
61
|
+
main_layout = QVBoxLayout()
|
|
62
|
+
|
|
63
|
+
# Header (Home Button)
|
|
64
|
+
header = QHBoxLayout()
|
|
65
|
+
btn_back = QPushButton("ā Home")
|
|
66
|
+
btn_back.clicked.connect(self.go_home.emit)
|
|
67
|
+
btn_back.setFixedWidth(100)
|
|
68
|
+
btn_back.setStyleSheet("""
|
|
69
|
+
QPushButton { background-color: #2E8B57; border-radius: 8px; color: white; }
|
|
70
|
+
QPushButton:hover { background-color: #689F38; }
|
|
71
|
+
""")
|
|
72
|
+
header.addWidget(btn_back)
|
|
73
|
+
|
|
74
|
+
title = QLabel("Single LiDAR Calibration")
|
|
75
|
+
title.setStyleSheet("font-size: 20px; font-weight: bold; color: #4CAF50;")
|
|
76
|
+
title.setAlignment(Qt.AlignCenter)
|
|
77
|
+
header.addWidget(title)
|
|
78
|
+
header.addStretch()
|
|
79
|
+
main_layout.addLayout(header)
|
|
80
|
+
|
|
81
|
+
# Tabs
|
|
82
|
+
self.tabs = QTabWidget()
|
|
83
|
+
self.tabs.setStyleSheet("""
|
|
84
|
+
QTabBar::tab {
|
|
85
|
+
min-width: 150px;
|
|
86
|
+
padding: 10px;
|
|
87
|
+
font-weight: bold;
|
|
88
|
+
}
|
|
89
|
+
QTabBar::tab:selected {
|
|
90
|
+
background-color: #2E8B57;
|
|
91
|
+
color: white;
|
|
92
|
+
}
|
|
93
|
+
""")
|
|
94
|
+
|
|
95
|
+
# Tab 1: Recorder
|
|
96
|
+
self.tab_record = self.create_recorder_ui()
|
|
97
|
+
self.tabs.addTab(self.tab_record, "Record / Capture Data")
|
|
98
|
+
|
|
99
|
+
# Tab 2: Calibration
|
|
100
|
+
self.tab_files = self.create_file_ui()
|
|
101
|
+
self.tabs.addTab(self.tab_files, "Calibrate from Files")
|
|
102
|
+
|
|
103
|
+
main_layout.addWidget(self.tabs)
|
|
104
|
+
self.setLayout(main_layout)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def create_recorder_ui(self):
|
|
109
|
+
w = QWidget()
|
|
110
|
+
layout = QVBoxLayout()
|
|
111
|
+
layout.setSpacing(6) # Adjusted to 5px (+1)
|
|
112
|
+
layout.setContentsMargins(5, 5, 5, 5)
|
|
113
|
+
|
|
114
|
+
# Reduced top margin stye for GroupBoxes to save vertical space
|
|
115
|
+
group_style = """
|
|
116
|
+
QGroupBox {
|
|
117
|
+
font-weight: bold;
|
|
118
|
+
border: 1px solid #555;
|
|
119
|
+
border-radius: 5px;
|
|
120
|
+
margin-top: 7px; /* Compact top margin */
|
|
121
|
+
padding-top: 5px;
|
|
122
|
+
}
|
|
123
|
+
QGroupBox::title {
|
|
124
|
+
subcontrol-origin: margin;
|
|
125
|
+
left: 10px;
|
|
126
|
+
padding: 0 3px 0 3px;
|
|
127
|
+
}
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
# 1. Source Type
|
|
131
|
+
type_group = QGroupBox("Input Type")
|
|
132
|
+
type_group.setStyleSheet(group_style)
|
|
133
|
+
type_layout = QHBoxLayout()
|
|
134
|
+
type_layout.setContentsMargins(5, 5, 5, 5) # Slight internal padding
|
|
135
|
+
self.rb_live = QRadioButton("Live Stream (ROS2)")
|
|
136
|
+
self.rb_bag = QRadioButton("Rosbag Recording")
|
|
137
|
+
self.rb_live.setChecked(True)
|
|
138
|
+
self.rb_live.toggled.connect(self.update_rec_ui_state)
|
|
139
|
+
type_layout.addWidget(self.rb_live)
|
|
140
|
+
type_layout.addWidget(self.rb_bag)
|
|
141
|
+
type_group.setLayout(type_layout)
|
|
142
|
+
layout.addWidget(type_group, 0) # No stretch
|
|
143
|
+
|
|
144
|
+
# 2. Configuration
|
|
145
|
+
self.config_stack = QStackedWidget()
|
|
146
|
+
# No fixed height, let it size naturally but minimally
|
|
147
|
+
|
|
148
|
+
# Live
|
|
149
|
+
live_w = QWidget()
|
|
150
|
+
live_l = QHBoxLayout()
|
|
151
|
+
live_l.setContentsMargins(0, 0, 0, 0)
|
|
152
|
+
|
|
153
|
+
# MULTI-SELECTION LIST
|
|
154
|
+
self.topic_list = QListWidget()
|
|
155
|
+
self.topic_list.setSelectionMode(QAbstractItemView.NoSelection) # We use checkboxes
|
|
156
|
+
self.topic_list.setFixedHeight(80) # Fixed height to not consume too much space
|
|
157
|
+
|
|
158
|
+
btn_refresh = QPushButton("š Scan Topics")
|
|
159
|
+
btn_refresh.setFixedWidth(150)
|
|
160
|
+
btn_refresh.clicked.connect(self.scan_topics)
|
|
161
|
+
|
|
162
|
+
live_l.addWidget(QLabel("LiDAR Topics:"))
|
|
163
|
+
live_l.addWidget(self.topic_list, 1)
|
|
164
|
+
live_l.addWidget(btn_refresh)
|
|
165
|
+
live_w.setLayout(live_l)
|
|
166
|
+
|
|
167
|
+
# Bag
|
|
168
|
+
bag_w = QWidget()
|
|
169
|
+
bag_l = QVBoxLayout()
|
|
170
|
+
bag_l.setContentsMargins(1, 1, 1, 1)
|
|
171
|
+
|
|
172
|
+
# Row 1: Folder
|
|
173
|
+
row_folder = QHBoxLayout()
|
|
174
|
+
self.bag_path = QLineEdit()
|
|
175
|
+
self.bag_path.setPlaceholderText("Path to Rosbag Folder")
|
|
176
|
+
btn_browse_bag = QPushButton("Browse")
|
|
177
|
+
btn_browse_bag.clicked.connect(self.browse_bag)
|
|
178
|
+
self.cb_loop = QCheckBox("Loop")
|
|
179
|
+
self.cb_loop.setChecked(True)
|
|
180
|
+
|
|
181
|
+
row_folder.addWidget(QLabel("Folder:"))
|
|
182
|
+
row_folder.addWidget(self.bag_path, 1)
|
|
183
|
+
row_folder.addWidget(btn_browse_bag)
|
|
184
|
+
row_folder.addWidget(self.cb_loop)
|
|
185
|
+
bag_l.addLayout(row_folder)
|
|
186
|
+
|
|
187
|
+
# Row 2: Topic Selection (List Widget style)
|
|
188
|
+
row_topic = QHBoxLayout()
|
|
189
|
+
row_topic.setContentsMargins(0, 0, 0, 0)
|
|
190
|
+
|
|
191
|
+
self.bag_topic_list = QListWidget()
|
|
192
|
+
self.bag_topic_list.setSelectionMode(QAbstractItemView.NoSelection)
|
|
193
|
+
self.bag_topic_list.setFixedHeight(80)
|
|
194
|
+
|
|
195
|
+
btn_scan_bag = QPushButton("š Scan Topics")
|
|
196
|
+
btn_scan_bag.setFixedWidth(150)
|
|
197
|
+
btn_scan_bag.clicked.connect(self.scan_bag_topics)
|
|
198
|
+
|
|
199
|
+
row_topic.addWidget(QLabel("LiDAR Topics:"))
|
|
200
|
+
row_topic.addWidget(self.bag_topic_list, 1)
|
|
201
|
+
row_topic.addWidget(btn_scan_bag)
|
|
202
|
+
bag_l.addLayout(row_topic)
|
|
203
|
+
|
|
204
|
+
# Info Label
|
|
205
|
+
lbl_info = QLabel("ā¹ļø LiDAR topic should be for 3D cloud (PointCloud2)")
|
|
206
|
+
lbl_info.setStyleSheet("color: #AAA; font-size: 10px; margin-left: 5px;")
|
|
207
|
+
bag_l.addWidget(lbl_info)
|
|
208
|
+
|
|
209
|
+
bag_w.setLayout(bag_l)
|
|
210
|
+
|
|
211
|
+
self.config_stack.addWidget(live_w)
|
|
212
|
+
self.config_stack.addWidget(bag_w)
|
|
213
|
+
layout.addWidget(self.config_stack, 0)
|
|
214
|
+
|
|
215
|
+
# 3. Output Settings (Expanded with RViz)
|
|
216
|
+
out_group = QGroupBox("Output & Visual Settings")
|
|
217
|
+
out_group.setStyleSheet(group_style)
|
|
218
|
+
out_layout = QHBoxLayout()
|
|
219
|
+
out_layout.setContentsMargins(6, 6, 6, 6)
|
|
220
|
+
|
|
221
|
+
# Sub-layout for Names
|
|
222
|
+
name_v = QVBoxLayout()
|
|
223
|
+
out_layout.addLayout(name_v, 1)
|
|
224
|
+
|
|
225
|
+
self.out_folder = QLineEdit()
|
|
226
|
+
self.out_folder.setPlaceholderText("Folder Name (e.g 'dataset_1')")
|
|
227
|
+
self.pose_prefix = QLineEdit()
|
|
228
|
+
self.pose_prefix.setPlaceholderText("Pose Prefix (e.g 'scan')")
|
|
229
|
+
|
|
230
|
+
row1 = QHBoxLayout()
|
|
231
|
+
row1.addWidget(QLabel("Folder:"))
|
|
232
|
+
row1.addWidget(self.out_folder)
|
|
233
|
+
name_v.addLayout(row1)
|
|
234
|
+
|
|
235
|
+
row2 = QHBoxLayout()
|
|
236
|
+
row2.addWidget(QLabel("Prefix:"))
|
|
237
|
+
row2.addWidget(self.pose_prefix)
|
|
238
|
+
name_v.addLayout(row2)
|
|
239
|
+
|
|
240
|
+
# RViz settings
|
|
241
|
+
rviz_v = QVBoxLayout()
|
|
242
|
+
out_layout.addLayout(rviz_v, 1)
|
|
243
|
+
|
|
244
|
+
self.rviz_combo = QComboBox()
|
|
245
|
+
self.scan_rviz_configs()
|
|
246
|
+
|
|
247
|
+
self.rviz_new = QLineEdit()
|
|
248
|
+
self.rviz_new.setPlaceholderText("New Config Name (optional)")
|
|
249
|
+
|
|
250
|
+
r1 = QHBoxLayout()
|
|
251
|
+
r1.addWidget(QLabel("RViz Config:"))
|
|
252
|
+
r1.addWidget(self.rviz_combo)
|
|
253
|
+
rviz_v.addLayout(r1)
|
|
254
|
+
|
|
255
|
+
r2 = QHBoxLayout()
|
|
256
|
+
r2.addWidget(QLabel("Create New:"))
|
|
257
|
+
r2.addWidget(self.rviz_new)
|
|
258
|
+
rviz_v.addLayout(r2)
|
|
259
|
+
|
|
260
|
+
out_group.setLayout(out_layout)
|
|
261
|
+
layout.addWidget(out_group, 0)
|
|
262
|
+
|
|
263
|
+
# 4. Output Log
|
|
264
|
+
self.rec_log = QTextEdit()
|
|
265
|
+
self.rec_log.setReadOnly(True)
|
|
266
|
+
self.rec_log.setStyleSheet("background-color: #1e1e1e; color: #00ff00; font-family: Monospace;")
|
|
267
|
+
# Give the log window the stretch priority (1) while others are 0
|
|
268
|
+
layout.addWidget(self.rec_log, 1)
|
|
269
|
+
|
|
270
|
+
# 6. Controls
|
|
271
|
+
ctrl_layout = QHBoxLayout()
|
|
272
|
+
self.btn_start_rec = QPushButton("ā¶ Start")
|
|
273
|
+
self.btn_start_rec.setStyleSheet("""
|
|
274
|
+
QPushButton { background-color: #4CAF50; color: white; padding: 10px; font-weight: bold; }
|
|
275
|
+
QPushButton:hover { background-color: #66BB6A; }
|
|
276
|
+
""")
|
|
277
|
+
self.btn_start_rec.clicked.connect(self.start_listening)
|
|
278
|
+
|
|
279
|
+
self.btn_play_pause = QPushButton("⯠Play/Pause")
|
|
280
|
+
self.btn_play_pause.setStyleSheet("""
|
|
281
|
+
QPushButton { background-color: #2196F3; color: white; padding: 10px; }
|
|
282
|
+
QPushButton:hover { background-color: #42A5F5; }
|
|
283
|
+
""")
|
|
284
|
+
self.btn_play_pause.clicked.connect(self.toggle_pause)
|
|
285
|
+
self.btn_play_pause.setEnabled(False)
|
|
286
|
+
|
|
287
|
+
self.btn_capture = QPushButton("šø Capture Frame")
|
|
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_pose)
|
|
293
|
+
self.btn_capture.setEnabled(False)
|
|
294
|
+
|
|
295
|
+
self.btn_stop_rec = QPushButton("ā¹ Stop")
|
|
296
|
+
self.btn_stop_rec.setStyleSheet("""
|
|
297
|
+
QPushButton { background-color: #d32f2f; color: white; padding: 10px; }
|
|
298
|
+
QPushButton:hover { background-color: #ef5350; }
|
|
299
|
+
""")
|
|
300
|
+
self.btn_stop_rec.clicked.connect(self.stop_listening)
|
|
301
|
+
self.btn_stop_rec.setEnabled(False)
|
|
302
|
+
|
|
303
|
+
ctrl_layout.addWidget(self.btn_start_rec)
|
|
304
|
+
ctrl_layout.addWidget(self.btn_play_pause)
|
|
305
|
+
ctrl_layout.addWidget(self.btn_capture)
|
|
306
|
+
ctrl_layout.addWidget(self.btn_stop_rec)
|
|
307
|
+
layout.addLayout(ctrl_layout, 0)
|
|
308
|
+
|
|
309
|
+
# 7. Back Button
|
|
310
|
+
btn_back = QPushButton("ā Back")
|
|
311
|
+
btn_back.clicked.connect(self.back_from_record)
|
|
312
|
+
btn_back.setStyleSheet("""
|
|
313
|
+
QPushButton { background-color: #2E8B57; border-radius: 8px; color: white; padding: 8px; font-weight: bold; }
|
|
314
|
+
QPushButton:hover { background-color: #689F38; }
|
|
315
|
+
""")
|
|
316
|
+
layout.addWidget(btn_back, 0)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
w.setLayout(layout)
|
|
320
|
+
return w
|
|
321
|
+
|
|
322
|
+
def create_file_ui(self):
|
|
323
|
+
w = QWidget()
|
|
324
|
+
layout = QVBoxLayout()
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
# Title
|
|
329
|
+
lbl = QLabel("Select 2 Point Clouds (PCD)")
|
|
330
|
+
lbl.setStyleSheet("font-size: 18px; font-weight: bold; margin: 10px;")
|
|
331
|
+
lbl.setAlignment(Qt.AlignCenter)
|
|
332
|
+
layout.addWidget(lbl)
|
|
333
|
+
|
|
334
|
+
# Form
|
|
335
|
+
form_group = QGroupBox()
|
|
336
|
+
form_layout = QVBoxLayout()
|
|
337
|
+
|
|
338
|
+
# Source
|
|
339
|
+
src_layout = QHBoxLayout()
|
|
340
|
+
self.src_path = QLineEdit()
|
|
341
|
+
self.src_path.setPlaceholderText("Path to Source .pcd")
|
|
342
|
+
self.src_path.textChanged.connect(self.validate_file_inputs)
|
|
343
|
+
btn_src = QPushButton("Browse")
|
|
344
|
+
btn_src.clicked.connect(lambda: self.browse_file(self.src_path))
|
|
345
|
+
src_layout.addWidget(QLabel("Source Cloud:"))
|
|
346
|
+
src_layout.addWidget(self.src_path)
|
|
347
|
+
src_layout.addWidget(btn_src)
|
|
348
|
+
form_layout.addLayout(src_layout)
|
|
349
|
+
|
|
350
|
+
# Target
|
|
351
|
+
tgt_layout = QHBoxLayout()
|
|
352
|
+
self.tgt_path = QLineEdit()
|
|
353
|
+
self.tgt_path.setPlaceholderText("Path to Target .pcd")
|
|
354
|
+
self.tgt_path.textChanged.connect(self.validate_file_inputs)
|
|
355
|
+
btn_tgt = QPushButton("Browse")
|
|
356
|
+
btn_tgt.clicked.connect(lambda: self.browse_file(self.tgt_path))
|
|
357
|
+
tgt_layout.addWidget(QLabel("Target Cloud:"))
|
|
358
|
+
tgt_layout.addWidget(self.tgt_path)
|
|
359
|
+
tgt_layout.addWidget(btn_tgt)
|
|
360
|
+
form_layout.addLayout(tgt_layout)
|
|
361
|
+
|
|
362
|
+
form_group.setLayout(form_layout)
|
|
363
|
+
layout.addWidget(form_group)
|
|
364
|
+
|
|
365
|
+
# Action
|
|
366
|
+
self.btn_calib = QPushButton("Begin Calibration")
|
|
367
|
+
self.btn_calib.clicked.connect(self.run_calibration)
|
|
368
|
+
self.btn_calib.setEnabled(False)
|
|
369
|
+
self.btn_calib.setStyleSheet("""
|
|
370
|
+
QPushButton {
|
|
371
|
+
background-color: #E65100;
|
|
372
|
+
color: white;
|
|
373
|
+
padding: 15px;
|
|
374
|
+
font-size: 16px;
|
|
375
|
+
font-weight: bold;
|
|
376
|
+
border-radius: 12px;
|
|
377
|
+
}
|
|
378
|
+
QPushButton:hover {
|
|
379
|
+
background-color: #F57C00;
|
|
380
|
+
}
|
|
381
|
+
QPushButton:disabled {
|
|
382
|
+
background-color: #555;
|
|
383
|
+
color: #888;
|
|
384
|
+
}
|
|
385
|
+
""")
|
|
386
|
+
layout.addWidget(self.btn_calib)
|
|
387
|
+
|
|
388
|
+
# Logs
|
|
389
|
+
layout.addWidget(QLabel("Process Output:"))
|
|
390
|
+
self.calib_log = QTextEdit()
|
|
391
|
+
self.calib_log.setReadOnly(True)
|
|
392
|
+
self.calib_log.setStyleSheet("font-family: Monospace; font-size: 12px; background-color: #1e1e1e; color: #00ff00;")
|
|
393
|
+
layout.addWidget(self.calib_log)
|
|
394
|
+
|
|
395
|
+
# Back Button
|
|
396
|
+
btn_back = QPushButton("ā Back")
|
|
397
|
+
btn_back.clicked.connect(self.go_home.emit)
|
|
398
|
+
btn_back.setStyleSheet("""
|
|
399
|
+
QPushButton { background-color: #2E8B57; border-radius: 8px; color: white; padding: 8px; font-weight: bold; }
|
|
400
|
+
QPushButton:hover { background-color: #689F38; }
|
|
401
|
+
""")
|
|
402
|
+
layout.addWidget(btn_back)
|
|
403
|
+
|
|
404
|
+
w.setLayout(layout)
|
|
405
|
+
return w
|
|
406
|
+
|
|
407
|
+
# --- Recorder Logic ---
|
|
408
|
+
def update_rec_ui_state(self):
|
|
409
|
+
is_live = self.rb_live.isChecked()
|
|
410
|
+
self.config_stack.setCurrentIndex(0 if is_live else 1)
|
|
411
|
+
self.btn_play_pause.setEnabled(False) # Reset
|
|
412
|
+
|
|
413
|
+
def scan_topics(self):
|
|
414
|
+
self.rec_log.append("Scanning ROS2 topics...")
|
|
415
|
+
self.topic_list.clear()
|
|
416
|
+
|
|
417
|
+
script_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../single_data_extractor.py"))
|
|
418
|
+
proc = subprocess.run(['python3', script_path, '--mode', 'discover', '--format', 'json'],
|
|
419
|
+
capture_output=True, text=True)
|
|
420
|
+
|
|
421
|
+
try:
|
|
422
|
+
data = json.loads(proc.stdout)
|
|
423
|
+
topics = data.get('lidar', [])
|
|
424
|
+
for t in topics:
|
|
425
|
+
item = QListWidgetItem(t)
|
|
426
|
+
item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
|
|
427
|
+
item.setCheckState(Qt.Unchecked)
|
|
428
|
+
self.topic_list.addItem(item)
|
|
429
|
+
|
|
430
|
+
self.rec_log.append(f"Found {len(topics)} LiDAR topics.")
|
|
431
|
+
except json.JSONDecodeError:
|
|
432
|
+
self.rec_log.append("Error scanning topics.")
|
|
433
|
+
|
|
434
|
+
def scan_bag_topics(self):
|
|
435
|
+
bag_path = self.bag_path.text().strip()
|
|
436
|
+
if not bag_path or not os.path.exists(bag_path):
|
|
437
|
+
QMessageBox.warning(self, "Warning", "Please select a valid rosbag folder first.")
|
|
438
|
+
return
|
|
439
|
+
|
|
440
|
+
self.rec_log.append("Scanning rosbag topics...")
|
|
441
|
+
self.bag_topic_list.clear() # Clear list
|
|
442
|
+
|
|
443
|
+
script_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../single_data_extractor.py"))
|
|
444
|
+
proc = subprocess.run(['python3', script_path, '--mode', 'discover_bag', '--bag', bag_path, '--format', 'json'],
|
|
445
|
+
capture_output=True, text=True)
|
|
446
|
+
|
|
447
|
+
try:
|
|
448
|
+
data = json.loads(proc.stdout)
|
|
449
|
+
lidar_topics = data.get('lidar', [])
|
|
450
|
+
|
|
451
|
+
# Populate ListWidget with Checkboxes
|
|
452
|
+
for t in lidar_topics:
|
|
453
|
+
if t == 'None': continue
|
|
454
|
+
item = QListWidgetItem(t)
|
|
455
|
+
item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
|
|
456
|
+
item.setCheckState(Qt.Unchecked)
|
|
457
|
+
self.bag_topic_list.addItem(item)
|
|
458
|
+
|
|
459
|
+
self.rec_log.append(f"Rosbag topics scanned. Found {self.bag_topic_list.count()} LiDAR topics.")
|
|
460
|
+
except json.JSONDecodeError:
|
|
461
|
+
self.rec_log.append("Error scanning topics.")
|
|
462
|
+
self.rec_log.append(f"Output: {proc.stdout}")
|
|
463
|
+
|
|
464
|
+
def scan_rviz_configs(self):
|
|
465
|
+
self.rviz_combo.clear()
|
|
466
|
+
# Default dir
|
|
467
|
+
config_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../rviz_configs"))
|
|
468
|
+
os.makedirs(config_dir, exist_ok=True)
|
|
469
|
+
|
|
470
|
+
files = glob.glob(os.path.join(config_dir, "*.rviz"))
|
|
471
|
+
names = [os.path.basename(f) for f in files]
|
|
472
|
+
|
|
473
|
+
if not names:
|
|
474
|
+
self.rviz_combo.addItem("default_calib.rviz")
|
|
475
|
+
else:
|
|
476
|
+
self.rviz_combo.addItems(sorted(names))
|
|
477
|
+
|
|
478
|
+
def browse_bag(self):
|
|
479
|
+
path = QFileDialog.getExistingDirectory(self, "Select Rosbag Folder", "")
|
|
480
|
+
if path:
|
|
481
|
+
self.bag_path.setText(path)
|
|
482
|
+
|
|
483
|
+
def start_listening(self):
|
|
484
|
+
script_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../single_data_extractor.py"))
|
|
485
|
+
is_live = self.rb_live.isChecked()
|
|
486
|
+
|
|
487
|
+
args = ["-u", script_path]
|
|
488
|
+
|
|
489
|
+
if is_live:
|
|
490
|
+
# Collect checked items
|
|
491
|
+
selected_topics = []
|
|
492
|
+
for i in range(self.topic_list.count()):
|
|
493
|
+
item = self.topic_list.item(i)
|
|
494
|
+
if item.checkState() == Qt.Checked:
|
|
495
|
+
selected_topics.append(item.text())
|
|
496
|
+
|
|
497
|
+
if not selected_topics:
|
|
498
|
+
QMessageBox.warning(self, "Warning", "Select at least one LiDAR topic.")
|
|
499
|
+
return
|
|
500
|
+
|
|
501
|
+
# Join with commas
|
|
502
|
+
topic_str = ",".join(selected_topics)
|
|
503
|
+
args.extend(['--mode', 'live', '--lidar_topic', topic_str])
|
|
504
|
+
self.btn_play_pause.setEnabled(True) # Enabled for Master Pause feature
|
|
505
|
+
else:
|
|
506
|
+
bag = self.bag_path.text().strip()
|
|
507
|
+
if not os.path.exists(bag):
|
|
508
|
+
QMessageBox.warning(self, "Warning", "Select a valid rosbag folder.")
|
|
509
|
+
return
|
|
510
|
+
args.extend(['--mode', 'bag', '--bag', bag])
|
|
511
|
+
if not self.cb_loop.isChecked():
|
|
512
|
+
args.append('--play_once')
|
|
513
|
+
|
|
514
|
+
# Add topics if checked
|
|
515
|
+
bag_topics = []
|
|
516
|
+
for i in range(self.bag_topic_list.count()):
|
|
517
|
+
item = self.bag_topic_list.item(i)
|
|
518
|
+
if item.checkState() == Qt.Checked:
|
|
519
|
+
bag_topics.append(item.text())
|
|
520
|
+
|
|
521
|
+
if bag_topics:
|
|
522
|
+
# Comma separated for multi-topic support
|
|
523
|
+
args.extend(['--lidar_topic', ",".join(bag_topics)])
|
|
524
|
+
else:
|
|
525
|
+
QMessageBox.warning(self, "Warning", "Please select at least one LiDAR topic from the list.")
|
|
526
|
+
return
|
|
527
|
+
|
|
528
|
+
self.btn_play_pause.setEnabled(True)
|
|
529
|
+
|
|
530
|
+
# args.extend(['--calib_mode', 'single_lidar']) # Deprecated in single_data_extractor
|
|
531
|
+
|
|
532
|
+
# Naming Args
|
|
533
|
+
folder_name = self.out_folder.text().strip()
|
|
534
|
+
if folder_name:
|
|
535
|
+
args.extend(['--output_name', folder_name])
|
|
536
|
+
|
|
537
|
+
pose_prefix = self.pose_prefix.text().strip()
|
|
538
|
+
if pose_prefix:
|
|
539
|
+
args.extend(['--pose_prefix', pose_prefix])
|
|
540
|
+
|
|
541
|
+
# RViz Logic
|
|
542
|
+
new_rviz = self.rviz_new.text().strip()
|
|
543
|
+
selected_rviz = self.rviz_combo.currentText().strip()
|
|
544
|
+
|
|
545
|
+
if new_rviz:
|
|
546
|
+
if not new_rviz.endswith('.rviz'): new_rviz += '.rviz'
|
|
547
|
+
args.extend(['--rviz_config', new_rviz])
|
|
548
|
+
elif selected_rviz:
|
|
549
|
+
args.extend(['--rviz_config', selected_rviz])
|
|
550
|
+
|
|
551
|
+
self.rec_log.clear()
|
|
552
|
+
self.rec_log.append(f"š¢ Starting Process: {'Live' if is_live else 'Bag'} Mode")
|
|
553
|
+
|
|
554
|
+
self.record_process = QProcess()
|
|
555
|
+
self.record_process.readyReadStandardOutput.connect(self.handle_rec_stdout)
|
|
556
|
+
self.record_process.readyReadStandardError.connect(self.handle_rec_stderr)
|
|
557
|
+
self.record_process.finished.connect(self.on_rec_finished)
|
|
558
|
+
|
|
559
|
+
self.record_process.start("python3", args)
|
|
560
|
+
|
|
561
|
+
self.btn_start_rec.setEnabled(False)
|
|
562
|
+
self.btn_capture.setEnabled(True)
|
|
563
|
+
self.btn_stop_rec.setEnabled(True)
|
|
564
|
+
|
|
565
|
+
# Disable inputs
|
|
566
|
+
self.rb_live.setEnabled(False)
|
|
567
|
+
self.rb_bag.setEnabled(False)
|
|
568
|
+
self.out_folder.setEnabled(False)
|
|
569
|
+
self.pose_prefix.setEnabled(False)
|
|
570
|
+
self.rviz_combo.setEnabled(False)
|
|
571
|
+
self.rviz_new.setEnabled(False)
|
|
572
|
+
self.topic_list.setEnabled(False)
|
|
573
|
+
|
|
574
|
+
def stop_listening(self):
|
|
575
|
+
if self.record_process and self.record_process.state() == QProcess.Running:
|
|
576
|
+
self.record_process.write(b'q\n')
|
|
577
|
+
self.record_process.waitForFinished(1000)
|
|
578
|
+
if self.record_process.state() == QProcess.Running:
|
|
579
|
+
self.record_process.kill()
|
|
580
|
+
# Finished signal will handle UI reset
|
|
581
|
+
|
|
582
|
+
def on_rec_finished(self):
|
|
583
|
+
self.btn_start_rec.setEnabled(True)
|
|
584
|
+
self.btn_capture.setEnabled(False)
|
|
585
|
+
self.btn_stop_rec.setEnabled(False)
|
|
586
|
+
self.btn_play_pause.setEnabled(False)
|
|
587
|
+
|
|
588
|
+
# Re-enable inputs
|
|
589
|
+
self.rb_live.setEnabled(True)
|
|
590
|
+
self.rb_bag.setEnabled(True)
|
|
591
|
+
self.out_folder.setEnabled(True)
|
|
592
|
+
self.pose_prefix.setEnabled(True)
|
|
593
|
+
self.rviz_combo.setEnabled(True)
|
|
594
|
+
self.rviz_new.setEnabled(True)
|
|
595
|
+
self.topic_list.setEnabled(True)
|
|
596
|
+
|
|
597
|
+
# SCAN REFRESH in case a new RViz was created
|
|
598
|
+
self.scan_rviz_configs()
|
|
599
|
+
|
|
600
|
+
self.rec_log.append("\nā
Process Stopped.")
|
|
601
|
+
|
|
602
|
+
def capture_pose(self):
|
|
603
|
+
if self.record_process and self.record_process.state() == QProcess.Running:
|
|
604
|
+
self.record_process.write(b'n\n')
|
|
605
|
+
|
|
606
|
+
def toggle_pause(self):
|
|
607
|
+
if self.record_process and self.record_process.state() == QProcess.Running:
|
|
608
|
+
self.record_process.write(b'p\n')
|
|
609
|
+
|
|
610
|
+
def handle_rec_stdout(self):
|
|
611
|
+
data = self.record_process.readAllStandardOutput()
|
|
612
|
+
self.rec_log.append(data.data().decode().strip())
|
|
613
|
+
|
|
614
|
+
def handle_rec_stderr(self):
|
|
615
|
+
data = self.record_process.readAllStandardError()
|
|
616
|
+
self.rec_log.append(data.data().decode().strip())
|
|
617
|
+
|
|
618
|
+
def back_from_record(self):
|
|
619
|
+
self.stop_listening()
|
|
620
|
+
self.go_home.emit()
|
|
621
|
+
|
|
622
|
+
# --- Calibration Logic ---
|
|
623
|
+
def browse_file(self, line_edit):
|
|
624
|
+
fname, _ = QFileDialog.getOpenFileName(self, "Select PCD", "", "Point Clouds (*.pcd)")
|
|
625
|
+
if fname:
|
|
626
|
+
line_edit.setText(fname)
|
|
627
|
+
|
|
628
|
+
def validate_file_inputs(self):
|
|
629
|
+
src = self.src_path.text().strip()
|
|
630
|
+
tgt = self.tgt_path.text().strip()
|
|
631
|
+
valid = bool(src) and bool(tgt) and os.path.exists(src) and os.path.exists(tgt)
|
|
632
|
+
self.btn_calib.setEnabled(valid)
|
|
633
|
+
|
|
634
|
+
def run_calibration(self):
|
|
635
|
+
src = self.src_path.text()
|
|
636
|
+
tgt = self.tgt_path.text()
|
|
637
|
+
|
|
638
|
+
self.calib_log.clear()
|
|
639
|
+
self.calib_log.append(f"Starting Calibration...\nSource: {src}\nTarget: {tgt}")
|
|
640
|
+
self.btn_calib.setEnabled(False)
|
|
641
|
+
self.btn_calib.setText("Running...")
|
|
642
|
+
|
|
643
|
+
self.calib_process = QProcess()
|
|
644
|
+
self.calib_process.readyReadStandardOutput.connect(self.handle_calib_stdout)
|
|
645
|
+
self.calib_process.readyReadStandardError.connect(self.handle_calib_stderr)
|
|
646
|
+
self.calib_process.finished.connect(self.on_calib_finished)
|
|
647
|
+
|
|
648
|
+
script_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../single_lidar_calib.py"))
|
|
649
|
+
|
|
650
|
+
cmd = "python3"
|
|
651
|
+
args = ["-u", script_path, "--src", src, "--tgt", tgt]
|
|
652
|
+
self.calib_process.start(cmd, args)
|
|
653
|
+
|
|
654
|
+
def handle_calib_stdout(self):
|
|
655
|
+
data = self.calib_process.readAllStandardOutput()
|
|
656
|
+
self.calib_log.append(data.data().decode())
|
|
657
|
+
|
|
658
|
+
def handle_calib_stderr(self):
|
|
659
|
+
data = self.calib_process.readAllStandardError()
|
|
660
|
+
self.calib_log.append(data.data().decode())
|
|
661
|
+
|
|
662
|
+
def on_calib_finished(self):
|
|
663
|
+
self.btn_calib.setEnabled(True)
|
|
664
|
+
self.btn_calib.setText("Begin Calibration")
|
|
665
|
+
self.calib_log.append("\nā
Calibration Process Finished.")
|
|
666
|
+
|
|
667
|
+
def cleanup(self):
|
|
668
|
+
if self.calib_process and self.calib_process.state() == QProcess.Running:
|
|
669
|
+
self.calib_process.kill()
|
|
670
|
+
if self.record_process and self.record_process.state() == QProcess.Running:
|
|
671
|
+
self.record_process.kill()
|
|
672
|
+
if self.diag_process and self.diag_process.state() == QProcess.Running:
|
|
673
|
+
self.diag_process.kill()
|