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,490 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8">
|
|
6
|
+
<title>3D Calibration Viewer</title>
|
|
7
|
+
<style>
|
|
8
|
+
body {
|
|
9
|
+
margin: 0;
|
|
10
|
+
overflow: hidden;
|
|
11
|
+
background-color: #000;
|
|
12
|
+
color: white;
|
|
13
|
+
user-select: none;
|
|
14
|
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
#info {
|
|
18
|
+
position: absolute;
|
|
19
|
+
top: 10px;
|
|
20
|
+
left: 10px;
|
|
21
|
+
background: rgba(0, 0, 0, 0.7);
|
|
22
|
+
padding: 15px;
|
|
23
|
+
border-radius: 5px;
|
|
24
|
+
pointer-events: none;
|
|
25
|
+
font-family: monospace;
|
|
26
|
+
font-size: 0.9em;
|
|
27
|
+
z-index: 10;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
#controls {
|
|
31
|
+
position: absolute;
|
|
32
|
+
top: 10px;
|
|
33
|
+
right: 10px;
|
|
34
|
+
background: rgba(30, 30, 30, 0.9);
|
|
35
|
+
padding: 15px;
|
|
36
|
+
border-radius: 5px;
|
|
37
|
+
max-width: 350px;
|
|
38
|
+
max-height: 85vh;
|
|
39
|
+
overflow-y: auto;
|
|
40
|
+
z-index: 20;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.control-section {
|
|
44
|
+
margin-bottom: 15px;
|
|
45
|
+
padding-bottom: 15px;
|
|
46
|
+
border-bottom: 1px solid #444;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.control-section:last-child {
|
|
50
|
+
border-bottom: none;
|
|
51
|
+
margin-bottom: 0;
|
|
52
|
+
padding-bottom: 0;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.section-title {
|
|
56
|
+
font-weight: bold;
|
|
57
|
+
color: #2196F3;
|
|
58
|
+
margin-bottom: 10px;
|
|
59
|
+
font-size: 0.95em;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
button {
|
|
63
|
+
background-color: #2196F3;
|
|
64
|
+
color: white;
|
|
65
|
+
border: none;
|
|
66
|
+
padding: 8px 12px;
|
|
67
|
+
border-radius: 4px;
|
|
68
|
+
cursor: pointer;
|
|
69
|
+
font-size: 0.9em;
|
|
70
|
+
margin-top: 5px;
|
|
71
|
+
width: 100%;
|
|
72
|
+
transition: background-color 0.2s;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
button:hover {
|
|
76
|
+
background-color: #1976D2;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
button:disabled {
|
|
80
|
+
background-color: #666;
|
|
81
|
+
cursor: not-allowed;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
#btnStartTest {
|
|
85
|
+
background-color: #4CAF50;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
#btnStartTest:hover:not(:disabled) {
|
|
89
|
+
background-color: #45a049;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
#fileInput {
|
|
93
|
+
display: none;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.file-status {
|
|
97
|
+
font-size: 0.85em;
|
|
98
|
+
padding: 10px;
|
|
99
|
+
background: rgba(255, 255, 255, 0.05);
|
|
100
|
+
border-radius: 4px;
|
|
101
|
+
margin-top: 10px;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.status-row {
|
|
105
|
+
display: flex;
|
|
106
|
+
justify-content: space-between;
|
|
107
|
+
align-items: center;
|
|
108
|
+
padding: 5px 0;
|
|
109
|
+
border-bottom: 1px solid #333;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.status-row:last-child {
|
|
113
|
+
border-bottom: none;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.status-label {
|
|
117
|
+
font-weight: 500;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.status-badge {
|
|
121
|
+
display: inline-block;
|
|
122
|
+
padding: 2px 8px;
|
|
123
|
+
border-radius: 3px;
|
|
124
|
+
font-size: 0.8em;
|
|
125
|
+
font-weight: bold;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.badge-ok {
|
|
129
|
+
background-color: #4CAF50;
|
|
130
|
+
color: white;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.badge-pending {
|
|
134
|
+
background-color: #FF9800;
|
|
135
|
+
color: white;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.hints {
|
|
139
|
+
font-size: 0.8em;
|
|
140
|
+
color: #aaa;
|
|
141
|
+
margin-top: 10px;
|
|
142
|
+
line-height: 1.5;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.spinner {
|
|
146
|
+
display: inline-block;
|
|
147
|
+
width: 12px;
|
|
148
|
+
height: 12px;
|
|
149
|
+
border: 2px solid #f3f3f3;
|
|
150
|
+
border-top: 2px solid #2196F3;
|
|
151
|
+
border-radius: 50%;
|
|
152
|
+
animation: spin 1s linear infinite;
|
|
153
|
+
margin-right: 5px;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
@keyframes spin {
|
|
157
|
+
0% { transform: rotate(0deg); }
|
|
158
|
+
100% { transform: rotate(360deg); }
|
|
159
|
+
}
|
|
160
|
+
</style>
|
|
161
|
+
<!-- Three.js CDN -->
|
|
162
|
+
<script type="importmap">
|
|
163
|
+
{
|
|
164
|
+
"imports": {
|
|
165
|
+
"three": "https://unpkg.com/three@0.160.0/build/three.module.js",
|
|
166
|
+
"three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
</script>
|
|
170
|
+
</head>
|
|
171
|
+
|
|
172
|
+
<body>
|
|
173
|
+
<div id="info">
|
|
174
|
+
<strong>Calibration Viewer</strong><br>
|
|
175
|
+
Robot: {{ calib['robot_id'] }}<br>
|
|
176
|
+
Time: {{ calib['timestamp'] | format_timestamp }}<br>
|
|
177
|
+
Score: {{ calib['score'] }} m<br>
|
|
178
|
+
<span id="loadStatus" style="font-size: 0.8em; color: #aaa;"></span>
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
<div id="controls">
|
|
182
|
+
<!-- Visualization Controls -->
|
|
183
|
+
<div class="control-section">
|
|
184
|
+
<div class="section-title">🎮 Visualization</div>
|
|
185
|
+
<button id="btnReset">Reset View</button>
|
|
186
|
+
<div class="hints">
|
|
187
|
+
LMB: Rotate | RMB: Pan | Scroll: Zoom
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
|
|
191
|
+
<!-- PCD File Management -->
|
|
192
|
+
<div class="control-section">
|
|
193
|
+
<div class="section-title">📁 Point Cloud Upload</div>
|
|
194
|
+
<button id="btnBrowsePCD">📂 Select PCD File</button>
|
|
195
|
+
<input type="file" id="fileInput" accept=".pcd" />
|
|
196
|
+
<button id="btnUploadPCD" style="background-color: #FF9800;" disabled>☁️ Upload PCD</button>
|
|
197
|
+
|
|
198
|
+
<div class="file-status">
|
|
199
|
+
<div class="status-row">
|
|
200
|
+
<span class="status-label">Selected:</span>
|
|
201
|
+
<span id="selectedFile" style="color: #aaa; font-size: 0.9em;">None</span>
|
|
202
|
+
</div>
|
|
203
|
+
<div class="status-row" style="margin-top: 5px;">
|
|
204
|
+
<span class="status-label">Size:</span>
|
|
205
|
+
<span id="selectedSize" style="color: #aaa;">-</span>
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
|
|
210
|
+
<!-- File Status Table -->
|
|
211
|
+
<div class="control-section">
|
|
212
|
+
<div class="section-title">📊 Calibration Files</div>
|
|
213
|
+
<div class="file-status">
|
|
214
|
+
<div class="status-row">
|
|
215
|
+
<span class="status-label">Extrinsic</span>
|
|
216
|
+
<span class="status-badge badge-ok">✓ Uploaded</span>
|
|
217
|
+
</div>
|
|
218
|
+
<div class="status-row">
|
|
219
|
+
<span class="status-label">Point Cloud</span>
|
|
220
|
+
<span id="pcdStatus" class="status-badge badge-pending">⏳ Pending</span>
|
|
221
|
+
</div>
|
|
222
|
+
</div>
|
|
223
|
+
</div>
|
|
224
|
+
|
|
225
|
+
<!-- Start Testing -->
|
|
226
|
+
<div class="control-section">
|
|
227
|
+
<button id="btnStartTest" disabled>▶️ Start Testing</button>
|
|
228
|
+
<div class="hints">
|
|
229
|
+
Upload PCD file and click to begin validation with extrinsic calibration.
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
|
|
233
|
+
<!-- Upload Progress -->
|
|
234
|
+
<div id="uploadProgress" class="control-section" style="display: none;">
|
|
235
|
+
<span class="spinner"></span>
|
|
236
|
+
<span id="progressText">Uploading...</span>
|
|
237
|
+
</div>
|
|
238
|
+
</div>
|
|
239
|
+
|
|
240
|
+
<script type="module">
|
|
241
|
+
import * as THREE from 'three';
|
|
242
|
+
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
|
243
|
+
import { PCDLoader } from 'three/addons/loaders/PCDLoader.js';
|
|
244
|
+
|
|
245
|
+
let camera, scene, renderer, controls;
|
|
246
|
+
let selectedFile = null;
|
|
247
|
+
let currentPCDUrl = "{{ file_url }}";
|
|
248
|
+
const robotId = "{{ calib['robot_id'] }}";
|
|
249
|
+
|
|
250
|
+
init();
|
|
251
|
+
setupEventListeners();
|
|
252
|
+
animate();
|
|
253
|
+
|
|
254
|
+
// ============ Initialization ============
|
|
255
|
+
function init() {
|
|
256
|
+
// Renderer
|
|
257
|
+
renderer = new THREE.WebGLRenderer({ antialias: true });
|
|
258
|
+
renderer.setPixelRatio(window.devicePixelRatio);
|
|
259
|
+
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
260
|
+
document.body.appendChild(renderer.domElement);
|
|
261
|
+
|
|
262
|
+
// Scene
|
|
263
|
+
scene = new THREE.Scene();
|
|
264
|
+
scene.background = new THREE.Color(0x000000);
|
|
265
|
+
|
|
266
|
+
// Camera
|
|
267
|
+
camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
|
|
268
|
+
camera.position.set(0, 5, 10);
|
|
269
|
+
camera.up.set(0, 0, 1);
|
|
270
|
+
|
|
271
|
+
// Controls
|
|
272
|
+
controls = new OrbitControls(camera, renderer.domElement);
|
|
273
|
+
controls.minDistance = 0.5;
|
|
274
|
+
controls.maxDistance = 50;
|
|
275
|
+
controls.target.set(0, 0, 0);
|
|
276
|
+
controls.update();
|
|
277
|
+
|
|
278
|
+
// Lights
|
|
279
|
+
scene.add(new THREE.AmbientLight(0x555555));
|
|
280
|
+
const dirLight = new THREE.DirectionalLight(0xffffff, 3);
|
|
281
|
+
dirLight.position.set(1, 1, 1);
|
|
282
|
+
scene.add(dirLight);
|
|
283
|
+
|
|
284
|
+
// Grid
|
|
285
|
+
const gridHelper = new THREE.GridHelper(20, 20, 0x444444, 0x222222);
|
|
286
|
+
gridHelper.rotation.x = Math.PI / 2;
|
|
287
|
+
scene.add(gridHelper);
|
|
288
|
+
|
|
289
|
+
// Axes
|
|
290
|
+
const axesHelper = new THREE.AxesHelper(1);
|
|
291
|
+
scene.add(axesHelper);
|
|
292
|
+
|
|
293
|
+
// Load initial PCD
|
|
294
|
+
loadPCD(currentPCDUrl);
|
|
295
|
+
|
|
296
|
+
// Events
|
|
297
|
+
window.addEventListener('resize', onWindowResize);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ============ Event Listeners ============
|
|
301
|
+
function setupEventListeners() {
|
|
302
|
+
document.getElementById('btnReset').onclick = () => controls.reset();
|
|
303
|
+
document.getElementById('btnBrowsePCD').onclick = () => document.getElementById('fileInput').click();
|
|
304
|
+
document.getElementById('fileInput').onchange = handleFileSelect;
|
|
305
|
+
document.getElementById('btnUploadPCD').onclick = uploadPCDFile;
|
|
306
|
+
document.getElementById('btnStartTest').onclick = startTesting;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function handleFileSelect(event) {
|
|
310
|
+
const file = event.target.files[0];
|
|
311
|
+
if (!file) return;
|
|
312
|
+
|
|
313
|
+
if (!file.name.toLowerCase().endsWith('.pcd')) {
|
|
314
|
+
alert('Please select a .pcd file');
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
selectedFile = file;
|
|
319
|
+
const fileName = file.name.length > 30 ? file.name.substring(0, 27) + '...' : file.name;
|
|
320
|
+
const fileSize = formatFileSize(file.size);
|
|
321
|
+
|
|
322
|
+
document.getElementById('selectedFile').textContent = fileName;
|
|
323
|
+
document.getElementById('selectedSize').textContent = fileSize;
|
|
324
|
+
document.getElementById('btnUploadPCD').disabled = false;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async function uploadPCDFile() {
|
|
328
|
+
if (!selectedFile) {
|
|
329
|
+
alert('Please select a file first');
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const formData = new FormData();
|
|
334
|
+
formData.append('artifacts', selectedFile);
|
|
335
|
+
formData.append('metadata', JSON.stringify({
|
|
336
|
+
robot_id: robotId,
|
|
337
|
+
score: 0.0
|
|
338
|
+
}));
|
|
339
|
+
|
|
340
|
+
showProgress(true, 'Uploading...');
|
|
341
|
+
document.getElementById('btnUploadPCD').disabled = true;
|
|
342
|
+
|
|
343
|
+
try {
|
|
344
|
+
const response = await fetch('/api/upload', {
|
|
345
|
+
method: 'POST',
|
|
346
|
+
body: formData
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
const result = await response.json();
|
|
350
|
+
|
|
351
|
+
if (response.ok) {
|
|
352
|
+
// Update the viewer with new PCD
|
|
353
|
+
const newFileUrl = `/files/${result.filename}`;
|
|
354
|
+
currentPCDUrl = newFileUrl;
|
|
355
|
+
|
|
356
|
+
// Clear scene and reload PCD
|
|
357
|
+
scene.children = scene.children.filter(child =>
|
|
358
|
+
!(child instanceof THREE.Points)
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
loadPCD(newFileUrl);
|
|
362
|
+
|
|
363
|
+
// Update status
|
|
364
|
+
updatePCDStatus(true);
|
|
365
|
+
document.getElementById('selectedFile').textContent = 'Uploaded ✓';
|
|
366
|
+
document.getElementById('selectedSize').textContent = result.file_size + ' bytes';
|
|
367
|
+
|
|
368
|
+
showProgress(false);
|
|
369
|
+
} else {
|
|
370
|
+
alert('Upload failed: ' + (result.error || 'Unknown error'));
|
|
371
|
+
showProgress(false);
|
|
372
|
+
document.getElementById('btnUploadPCD').disabled = false;
|
|
373
|
+
}
|
|
374
|
+
} catch (error) {
|
|
375
|
+
console.error('Upload error:', error);
|
|
376
|
+
alert('Upload error: ' + error.message);
|
|
377
|
+
showProgress(false);
|
|
378
|
+
document.getElementById('btnUploadPCD').disabled = false;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function loadPCD(fileUrl) {
|
|
383
|
+
if (!fileUrl || !fileUrl.toLowerCase().endsWith('.pcd')) {
|
|
384
|
+
showNonPCDMessage();
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const loader = new PCDLoader();
|
|
389
|
+
console.log('Loading PCD from:', fileUrl);
|
|
390
|
+
document.getElementById('loadStatus').textContent = 'Loading...';
|
|
391
|
+
|
|
392
|
+
loader.load(
|
|
393
|
+
fileUrl,
|
|
394
|
+
function (points) {
|
|
395
|
+
// Success
|
|
396
|
+
points.geometry.center();
|
|
397
|
+
points.geometry.rotateX(-Math.PI / 2);
|
|
398
|
+
points.material.color.setHex(0x00ff00);
|
|
399
|
+
points.material.size = 0.02;
|
|
400
|
+
scene.add(points);
|
|
401
|
+
console.log('PCD Loaded successfully');
|
|
402
|
+
document.getElementById('loadStatus').textContent = 'Loaded ✓';
|
|
403
|
+
},
|
|
404
|
+
function (progress) {
|
|
405
|
+
// Progress
|
|
406
|
+
const percent = (progress.loaded / progress.total * 100).toFixed(0);
|
|
407
|
+
document.getElementById('loadStatus').textContent = `Loading ${percent}%...`;
|
|
408
|
+
},
|
|
409
|
+
function (error) {
|
|
410
|
+
// Error
|
|
411
|
+
console.error('PCD loading error:', error);
|
|
412
|
+
document.getElementById('loadStatus').textContent = 'Load error ✗';
|
|
413
|
+
alert('Error loading PCD file. Is it a valid point cloud?');
|
|
414
|
+
}
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function showNonPCDMessage() {
|
|
419
|
+
const div = document.createElement('div');
|
|
420
|
+
div.style.position = 'absolute';
|
|
421
|
+
div.style.top = '50%';
|
|
422
|
+
div.style.width = '100%';
|
|
423
|
+
div.style.textAlign = 'center';
|
|
424
|
+
div.style.color = '#ff8800';
|
|
425
|
+
div.style.zIndex = '5';
|
|
426
|
+
div.innerHTML = '<h3>Visualization only available for .pcd files</h3><p>Use the file browser to upload a point cloud.</p>';
|
|
427
|
+
document.body.appendChild(div);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function updatePCDStatus(isUploaded) {
|
|
431
|
+
const badge = document.getElementById('pcdStatus');
|
|
432
|
+
const testBtn = document.getElementById('btnStartTest');
|
|
433
|
+
|
|
434
|
+
if (isUploaded) {
|
|
435
|
+
badge.textContent = '✓ Uploaded';
|
|
436
|
+
badge.className = 'status-badge badge-ok';
|
|
437
|
+
testBtn.disabled = false;
|
|
438
|
+
} else {
|
|
439
|
+
badge.textContent = '⏳ Pending';
|
|
440
|
+
badge.className = 'status-badge badge-pending';
|
|
441
|
+
testBtn.disabled = true;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function startTesting() {
|
|
446
|
+
const message = `Ready to test calibration!\n\n` +
|
|
447
|
+
`Robot: ${robotId}\n` +
|
|
448
|
+
`Extrinsic: ✓ Uploaded\n` +
|
|
449
|
+
`Point Cloud: ✓ Uploaded\n\n` +
|
|
450
|
+
`Next steps:\n` +
|
|
451
|
+
`1. Review the point cloud in the 3D viewer\n` +
|
|
452
|
+
`2. Check alignment quality\n` +
|
|
453
|
+
`3. Download files for deployment if satisfied`;
|
|
454
|
+
alert(message);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function showProgress(show, text = 'Uploading...') {
|
|
458
|
+
const progressDiv = document.getElementById('uploadProgress');
|
|
459
|
+
if (show) {
|
|
460
|
+
document.getElementById('progressText').textContent = text;
|
|
461
|
+
progressDiv.style.display = 'block';
|
|
462
|
+
} else {
|
|
463
|
+
progressDiv.style.display = 'none';
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function formatFileSize(bytes) {
|
|
468
|
+
if (bytes === 0) return '0 Bytes';
|
|
469
|
+
const k = 1024;
|
|
470
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
471
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
472
|
+
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// ============ Rendering ============
|
|
476
|
+
function onWindowResize() {
|
|
477
|
+
camera.aspect = window.innerWidth / window.innerHeight;
|
|
478
|
+
camera.updateProjectionMatrix();
|
|
479
|
+
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function animate() {
|
|
483
|
+
requestAnimationFrame(animate);
|
|
484
|
+
controls.update();
|
|
485
|
+
renderer.render(scene, camera);
|
|
486
|
+
}
|
|
487
|
+
</script>
|
|
488
|
+
</body>
|
|
489
|
+
|
|
490
|
+
</html>
|
fleet_server/utils.py
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""Fleet Server Utility Functions
|
|
2
|
+
|
|
3
|
+
Provides input validation, file handling, and logging utilities.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
import logging
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from .config import Config
|
|
11
|
+
|
|
12
|
+
# Configure logging
|
|
13
|
+
def setup_logging():
|
|
14
|
+
"""Setup logging for fleet server"""
|
|
15
|
+
log_file = Config.LOG_FILE
|
|
16
|
+
log_level = getattr(logging, Config.LOG_LEVEL, logging.INFO)
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger('fleet_server')
|
|
19
|
+
logger.setLevel(log_level)
|
|
20
|
+
|
|
21
|
+
# Ensure log directory exists
|
|
22
|
+
log_dir = os.path.dirname(log_file)
|
|
23
|
+
if log_dir:
|
|
24
|
+
os.makedirs(log_dir, exist_ok=True)
|
|
25
|
+
|
|
26
|
+
# File handler
|
|
27
|
+
file_handler = logging.FileHandler(log_file)
|
|
28
|
+
file_handler.setLevel(log_level)
|
|
29
|
+
|
|
30
|
+
# Console handler
|
|
31
|
+
console_handler = logging.StreamHandler()
|
|
32
|
+
console_handler.setLevel(log_level)
|
|
33
|
+
|
|
34
|
+
# Formatter
|
|
35
|
+
formatter = logging.Formatter(
|
|
36
|
+
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
37
|
+
)
|
|
38
|
+
file_handler.setFormatter(formatter)
|
|
39
|
+
console_handler.setFormatter(formatter)
|
|
40
|
+
|
|
41
|
+
logger.addHandler(file_handler)
|
|
42
|
+
logger.addHandler(console_handler)
|
|
43
|
+
|
|
44
|
+
return logger
|
|
45
|
+
|
|
46
|
+
logger = setup_logging()
|
|
47
|
+
|
|
48
|
+
def sanitize_filename(filename):
|
|
49
|
+
"""
|
|
50
|
+
Sanitize filename to prevent directory traversal attacks.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
filename: Original filename from upload
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Safe filename string
|
|
57
|
+
"""
|
|
58
|
+
# Remove path components
|
|
59
|
+
filename = os.path.basename(filename)
|
|
60
|
+
|
|
61
|
+
# Remove potentially dangerous characters
|
|
62
|
+
# Keep only alphanumeric, dash, underscore, dot
|
|
63
|
+
filename = re.sub(r'[^\w\-\.]', '_', filename)
|
|
64
|
+
|
|
65
|
+
# Remove multiple dots (prevents .pcd.exe attacks)
|
|
66
|
+
filename = re.sub(r'\.{2,}', '.', filename)
|
|
67
|
+
|
|
68
|
+
# Limit length
|
|
69
|
+
name, ext = os.path.splitext(filename)
|
|
70
|
+
if len(name) > 100:
|
|
71
|
+
name = name[:100]
|
|
72
|
+
filename = name + ext
|
|
73
|
+
|
|
74
|
+
return filename
|
|
75
|
+
|
|
76
|
+
def validate_file_extension(filename, allowed=None):
|
|
77
|
+
"""
|
|
78
|
+
Validate file extension against allowed list.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
filename: Filename to validate
|
|
82
|
+
allowed: Set of allowed extensions (defaults to Config.ALLOWED_EXTENSIONS)
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
True if extension is allowed, False otherwise
|
|
86
|
+
"""
|
|
87
|
+
if allowed is None:
|
|
88
|
+
allowed = Config.ALLOWED_EXTENSIONS
|
|
89
|
+
|
|
90
|
+
_, ext = os.path.splitext(filename)
|
|
91
|
+
ext = ext.lstrip('.').lower()
|
|
92
|
+
|
|
93
|
+
return ext in allowed
|
|
94
|
+
|
|
95
|
+
def validate_file_size(file_obj, max_size=None):
|
|
96
|
+
"""
|
|
97
|
+
Validate file size.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
file_obj: File-like object to validate
|
|
101
|
+
max_size: Maximum size in bytes (defaults to Config.MAX_UPLOAD_SIZE)
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
True if within size limit, False otherwise
|
|
105
|
+
"""
|
|
106
|
+
if max_size is None:
|
|
107
|
+
max_size = Config.MAX_UPLOAD_SIZE
|
|
108
|
+
|
|
109
|
+
file_obj.seek(0, os.SEEK_END)
|
|
110
|
+
size = file_obj.tell()
|
|
111
|
+
file_obj.seek(0)
|
|
112
|
+
|
|
113
|
+
return size <= max_size
|
|
114
|
+
|
|
115
|
+
def validate_upload(file_obj, filename):
|
|
116
|
+
"""
|
|
117
|
+
Comprehensive validation for file uploads.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
file_obj: File object from request
|
|
121
|
+
filename: Filename from upload
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
Tuple (is_valid, error_message)
|
|
125
|
+
"""
|
|
126
|
+
# Check filename
|
|
127
|
+
if not filename or filename == '':
|
|
128
|
+
return False, "No filename provided"
|
|
129
|
+
|
|
130
|
+
# Check extension
|
|
131
|
+
if not validate_file_extension(filename):
|
|
132
|
+
return False, f"File type not allowed. Allowed: {', '.join(Config.ALLOWED_EXTENSIONS)}"
|
|
133
|
+
|
|
134
|
+
# Check file size
|
|
135
|
+
if not validate_file_size(file_obj):
|
|
136
|
+
max_mb = Config.MAX_UPLOAD_SIZE / (1024 * 1024)
|
|
137
|
+
return False, f"File too large (max {max_mb:.0f}MB)"
|
|
138
|
+
|
|
139
|
+
return True, "Valid"
|
|
140
|
+
|
|
141
|
+
def get_safe_path(base_dir, filename):
|
|
142
|
+
"""
|
|
143
|
+
Get safe absolute path within base directory.
|
|
144
|
+
Prevents directory traversal attacks.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
base_dir: Base directory (absolute path)
|
|
148
|
+
filename: Requested filename
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
Safe absolute path if within base_dir, None otherwise
|
|
152
|
+
"""
|
|
153
|
+
base = Path(base_dir).resolve()
|
|
154
|
+
requested = (base / filename).resolve()
|
|
155
|
+
|
|
156
|
+
# Ensure requested path is within base directory
|
|
157
|
+
try:
|
|
158
|
+
requested.relative_to(base)
|
|
159
|
+
return str(requested)
|
|
160
|
+
except ValueError:
|
|
161
|
+
logger.warning(f"Attempted directory traversal: {requested}")
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
def format_filesize(size_bytes):
|
|
165
|
+
"""
|
|
166
|
+
Format bytes to human-readable size.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
size_bytes: Size in bytes
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
Formatted string (e.g., "1.5 MB")
|
|
173
|
+
"""
|
|
174
|
+
for unit in ['B', 'KB', 'MB', 'GB']:
|
|
175
|
+
if size_bytes < 1024.0:
|
|
176
|
+
return f"{size_bytes:.2f} {unit}"
|
|
177
|
+
size_bytes /= 1024.0
|
|
178
|
+
return f"{size_bytes:.2f} TB"
|
gui/__init__.py
ADDED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
gui/assets/virya.jpg
ADDED
|
Binary file
|