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,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)
|