OussamaElfila21 commited on
Commit
c561c85
·
verified ·
1 Parent(s): d232f13

Update try_page.html

Browse files
Files changed (1) hide show
  1. try_page.html +778 -777
try_page.html CHANGED
@@ -1,778 +1,779 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>HTTP Image Upload Demo</title>
7
- <style>
8
- /* Reset and base styles */
9
- * {
10
- box-sizing: border-box;
11
- margin: 0;
12
- padding: 0;
13
- }
14
-
15
- body {
16
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
17
- color: #333;
18
- line-height: 1.6;
19
- }
20
-
21
- /* Demo section styling */
22
- .execution-section {
23
- max-width: 1200px;
24
- margin: 0 auto;
25
- padding: 2rem;
26
- background-color: #f8f9fa;
27
- border-radius: 8px;
28
- box-shadow: 0 4px 6px rgba(0,0,0,0.1);
29
- }
30
-
31
- .section-title {
32
- font-size: 2rem;
33
- color: #384B70;
34
- margin-bottom: 1rem;
35
- padding-bottom: 0.5rem;
36
- border-bottom: 2px solid #507687;
37
- }
38
-
39
- .demo-container {
40
- display: flex;
41
- flex-wrap: wrap;
42
- gap: 2rem;
43
- margin-top: 1.5rem;
44
- }
45
-
46
- .upload-container, .response-container {
47
- flex: 1;
48
- min-width: 300px;
49
- padding: 1.5rem;
50
- background-color: white;
51
- border-radius: 8px;
52
- box-shadow: 0 2px 4px rgba(0,0,0,0.05);
53
- }
54
-
55
- .container-title {
56
- font-size: 1.5rem;
57
- margin-bottom: 1rem;
58
- color: #384B70;
59
- }
60
-
61
- /* Upload area styling */
62
- .file-input-container {
63
- border: 2px dashed #ccc;
64
- border-radius: 5px;
65
- padding: 2rem;
66
- text-align: center;
67
- margin-bottom: 1rem;
68
- transition: all 0.3s ease;
69
- }
70
-
71
- .file-input-container:hover {
72
- border-color: #507687;
73
- background-color: #f8f9fa;
74
- }
75
-
76
- #fileInput {
77
- display: none;
78
- }
79
-
80
- .file-label {
81
- cursor: pointer;
82
- display: flex;
83
- flex-direction: column;
84
- align-items: center;
85
- gap: 0.5rem;
86
- }
87
-
88
- .file-icon {
89
- font-size: 2.5rem;
90
- color: #507687;
91
- width: 64px;
92
- height: 64px;
93
- }
94
- .file-placeholder {
95
- max-width: 100%;
96
- height: auto;
97
- margin-top: 1rem;
98
- border-radius: 4px;
99
- display: none;
100
- }
101
-
102
- #sendButton {
103
- background-color: #384B70;
104
- color: white;
105
- border: none;
106
- border-radius: 4px;
107
- padding: 0.75rem 1.5rem;
108
- font-size: 1rem;
109
- cursor: pointer;
110
- transition: background-color 0.3s;
111
- width: 100%;
112
- margin-top: 1rem;
113
- }
114
-
115
- #sendButton:disabled {
116
- background-color: #cccccc;
117
- cursor: not-allowed;
118
- }
119
-
120
- #sendButton:hover:not(:disabled) {
121
- background-color: #507687;
122
- }
123
-
124
- /* Response area styling */
125
- .response-output {
126
- height: 300px;
127
- overflow-y: auto;
128
- background-color: #f8f9fa;
129
- border: 1px solid #ddd;
130
- border-radius: 4px;
131
- padding: 1rem;
132
- font-family: monospace;
133
- white-space: pre-wrap;
134
- }
135
-
136
- /* Tabs styling */
137
- .tabs {
138
- display: flex;
139
- border-bottom: 1px solid #ddd;
140
- margin-bottom: 1rem;
141
- }
142
-
143
- .tab-button {
144
- padding: 0.5rem 1rem;
145
- background-color: #f1f1f1;
146
- border: none;
147
- cursor: pointer;
148
- transition: background-color 0.3s;
149
- font-size: 1rem;
150
- }
151
-
152
- .tab-button.active {
153
- background-color: #384B70;
154
- color: white;
155
- }
156
-
157
- .tab-content {
158
- display: none;
159
- height: 300px;
160
- }
161
-
162
- .tab-content.active {
163
- display: block;
164
- }
165
-
166
- /* Visualization area styling */
167
- #visualizationContainer {
168
- position: relative;
169
- height: 100%;
170
- overflow: auto;
171
- background-color: #f8f9fa;
172
- border: 1px solid #ddd;
173
- border-radius: 4px;
174
- }
175
-
176
- .detection-canvas {
177
- display: block;
178
- margin: 0 auto;
179
- }
180
-
181
- /* Utilities */
182
- #loading {
183
- display: none;
184
- margin-top: 1rem;
185
- color: #384B70;
186
- font-weight: bold;
187
- text-align: center;
188
- }
189
-
190
- #message {
191
- margin-top: 1rem;
192
- padding: 0.75rem;
193
- border-radius: 4px;
194
- text-align: center;
195
- display: none;
196
- }
197
-
198
- .error {
199
- background-color: #ffebee;
200
- color: #d32f2f;
201
- }
202
-
203
- .success {
204
- background-color: #e8f5e9;
205
- color: #388e3c;
206
- }
207
-
208
- .info {
209
- font-size: 0.9rem;
210
- color: #666;
211
- margin-top: 0.5rem;
212
- }
213
-
214
- .stats {
215
- margin-top: 1rem;
216
- font-size: 0.9rem;
217
- color: #666;
218
- }
219
-
220
- /* Debug output */
221
- #debugOutput {
222
- margin-top: 0.5rem;
223
- font-size: 0.8rem;
224
- color: #999;
225
- border-top: 1px dashed #ddd;
226
- padding-top: 0.5rem;
227
- display: none;
228
- }
229
- </style>
230
- </head>
231
- <body>
232
- <!-- Interactive Demo Section -->
233
- <section class="execution-section">
234
- <h2 class="section-title">Try It Yourself</h2>
235
- <p>Upload an image and see the object detection and depth estimation results in real-time.</p>
236
-
237
- <div class="demo-container">
238
- <!-- Upload Container -->
239
- <div class="upload-container">
240
- <h3 class="container-title">Upload Image</h3>
241
-
242
- <div class="file-input-container">
243
- <label for="fileInput" class="file-label">
244
- <img src="https://upload.wikimedia.org/wikipedia/commons/a/a1/Icons8_flat_folder.svg" class="file-icon"/>
245
- <span>Click to select image</span>
246
- <p class="info">PNG or JPEG, max 2MB</p>
247
- </label>
248
- <input type="file" accept="image/*" id="fileInput" />
249
- <img id="imagePreview" class="file-placeholder" alt="Image preview" />
250
- </div>
251
-
252
- <button id="sendButton" disabled>Process Image</button>
253
- <div id="loading">Processing your image...</div>
254
- <div id="message"></div>
255
-
256
- <div class="stats">
257
- <div id="imageSize"></div>
258
- <div id="processingTime"></div>
259
- </div>
260
-
261
- <div id="debugOutput"></div>
262
- </div>
263
-
264
- <!-- Response Container with Tabs -->
265
- <div class="response-container">
266
- <h3 class="container-title">Response</h3>
267
-
268
- <div class="tabs">
269
- <button class="tab-button active" data-tab="raw">Raw Output</button>
270
- <button class="tab-button" data-tab="visual">Visual Output</button>
271
- </div>
272
-
273
- <!-- Raw Output Tab -->
274
- <div id="rawTab" class="tab-content active">
275
- <pre class="response-output" id="responseOutput">// Response will appear here after processing</pre>
276
- </div>
277
-
278
- <!-- Visual Output Tab -->
279
- <div id="visualTab" class="tab-content">
280
- <div id="visualizationContainer">
281
- <canvas id="detectionCanvas" class="detection-canvas"></canvas>
282
- </div>
283
- </div>
284
- </div>
285
- </div>
286
- </section>
287
-
288
- <script>
289
- // DOM Elements
290
- const fileInput = document.getElementById('fileInput');
291
- const imagePreview = document.getElementById('imagePreview');
292
- const sendButton = document.getElementById('sendButton');
293
- const loading = document.getElementById('loading');
294
- const message = document.getElementById('message');
295
- const responseOutput = document.getElementById('responseOutput');
296
- const imageSizeInfo = document.getElementById('imageSize');
297
- const processingTimeInfo = document.getElementById('processingTime');
298
- const tabButtons = document.querySelectorAll('.tab-button');
299
- const tabContents = document.querySelectorAll('.tab-content');
300
- const detectionCanvas = document.getElementById('detectionCanvas');
301
- const ctx = detectionCanvas.getContext('2d');
302
- const debugOutput = document.getElementById('debugOutput');
303
-
304
- // Enable debug mode (set to false in production)
305
- const DEBUG = true;
306
-
307
- // API endpoint URL
308
- const API_URL = '/api/predict';
309
-
310
- let imageFile = null;
311
- let startTime = null;
312
- let originalImage = null;
313
- let processingWidth = 0;
314
- let processingHeight = 0;
315
- let responseData = null;
316
-
317
- // Tab switching functionality
318
- tabButtons.forEach(button => {
319
- button.addEventListener('click', () => {
320
- const tabName = button.getAttribute('data-tab');
321
-
322
- // Update button states
323
- tabButtons.forEach(btn => btn.classList.remove('active'));
324
- button.classList.add('active');
325
-
326
- // Update tab content visibility
327
- tabContents.forEach(content => content.classList.remove('active'));
328
- document.getElementById(tabName + 'Tab').classList.add('active');
329
-
330
- // If switching to visual tab and we have data, ensure visualization is rendered
331
- if (tabName === 'visual' && responseData && originalImage) {
332
- visualizeResults(originalImage, responseData);
333
- }
334
- });
335
- });
336
-
337
- // Handle file input change
338
- fileInput.addEventListener('change', (event) => {
339
- const file = event.target.files[0];
340
-
341
- // Clear previous selections
342
- imageFile = null;
343
- imagePreview.style.display = 'none';
344
- sendButton.disabled = true;
345
- originalImage = null;
346
- responseData = null;
347
-
348
- // Validate file
349
- if (!file) return;
350
-
351
- if (file.size > 2 * 1024 * 1024) {
352
- showMessage('File size exceeds 2MB limit.', 'error');
353
- return;
354
- }
355
-
356
- if (!['image/png', 'image/jpeg'].includes(file.type)) {
357
- showMessage('Only PNG and JPEG formats are supported.', 'error');
358
- return;
359
- }
360
-
361
- // Store file for upload
362
- imageFile = file;
363
-
364
- // Show image preview
365
- const reader = new FileReader();
366
- reader.onload = (e) => {
367
- const image = new Image();
368
- image.src = e.target.result;
369
-
370
- image.onload = () => {
371
- // Store original image for visualization
372
- originalImage = image;
373
-
374
- // Set preview
375
- imagePreview.src = e.target.result;
376
- imagePreview.style.display = 'block';
377
-
378
- // Update image info
379
- imageSizeInfo.textContent = `Original size: ${image.width}x${image.height} pixels`;
380
-
381
- // Calculate processing dimensions (for visualization)
382
- calculateProcessingDimensions(image.width, image.height);
383
-
384
- // Enable send button
385
- sendButton.disabled = false;
386
- showMessage('Image ready to process.', 'info');
387
- };
388
- };
389
- reader.readAsDataURL(file);
390
- });
391
-
392
- // Calculate dimensions for processing visualization
393
- function calculateProcessingDimensions(width, height) {
394
- const maxWidth = 640;
395
- const maxHeight = 320;
396
-
397
- // Calculate dimensions
398
- if (width > height) {
399
- if (width > maxWidth) {
400
- height = Math.round((height * maxWidth) / width);
401
- width = maxWidth;
402
- }
403
- } else {
404
- if (height > maxHeight) {
405
- width = Math.round((width * maxHeight) / height);
406
- height = maxHeight;
407
- }
408
- }
409
-
410
- // Store processing dimensions for visualization
411
- processingWidth = width;
412
- processingHeight = height;
413
- }
414
-
415
- // Handle send button click
416
- sendButton.addEventListener('click', async () => {
417
- if (!imageFile) {
418
- showMessage('No image selected.', 'error');
419
- return;
420
- }
421
-
422
- // Clear previous response
423
- responseOutput.textContent = "// Processing...";
424
- clearCanvas();
425
- responseData = null;
426
- debugOutput.style.display = 'none';
427
-
428
- // Show loading state
429
- loading.style.display = 'block';
430
- message.style.display = 'none';
431
-
432
- // Reset processing time
433
- processingTimeInfo.textContent = '';
434
-
435
- // Record start time
436
- startTime = performance.now();
437
-
438
- // Create form data for HTTP request
439
- const formData = new FormData();
440
- formData.append('file', imageFile);
441
-
442
- try {
443
- // Send HTTP request
444
- const response = await fetch(API_URL, {
445
- method: 'POST',
446
- body: formData
447
- });
448
-
449
- // Handle response
450
- if (!response.ok) {
451
- const errorText = await response.text();
452
- throw new Error(`HTTP error ${response.status}: ${errorText}`);
453
- }
454
-
455
- // Parse JSON response
456
- const data = await response.json();
457
- responseData = data;
458
-
459
- // Calculate processing time
460
- const endTime = performance.now();
461
- const timeTaken = endTime - startTime;
462
-
463
- // Format and display raw response
464
- responseOutput.textContent = JSON.stringify(data, null, 2);
465
- processingTimeInfo.textContent = `Processing time: ${timeTaken.toFixed(2)} ms`;
466
-
467
- // Visualize the results
468
- if (originalImage) {
469
- visualizeResults(originalImage, data);
470
- }
471
-
472
- // Show success message
473
- showMessage('Image processed successfully!', 'success');
474
- } catch (error) {
475
- console.error('Error processing image:', error);
476
- showMessage(`Error: ${error.message}`, 'error');
477
- responseOutput.textContent = `// Error: ${error.message}`;
478
-
479
- if (DEBUG) {
480
- debugOutput.style.display = 'block';
481
- debugOutput.textContent = `Error: ${error.message}\n${error.stack || ''}`;
482
- }
483
- } finally {
484
- loading.style.display = 'none';
485
- }
486
- });
487
-
488
- // Visualize detection results
489
- function visualizeResults(image, data) {
490
- try {
491
- // Set canvas dimensions
492
- detectionCanvas.width = processingWidth;
493
- detectionCanvas.height = processingHeight;
494
-
495
- // Draw the original image
496
- ctx.drawImage(image, 0, 0, processingWidth, processingHeight);
497
-
498
- // Set styles for bounding boxes
499
- ctx.lineWidth = 3;
500
- ctx.font = 'bold 14px Arial';
501
-
502
- // Find detections (checking all common formats)
503
- let detections = [];
504
- let detectionSource = '';
505
-
506
- if (data.detections && Array.isArray(data.detections)) {
507
- detections = data.detections;
508
- detectionSource = 'detections';
509
- } else if (data.predictions && Array.isArray(data.predictions)) {
510
- detections = data.predictions;
511
- detectionSource = 'predictions';
512
- } else if (data.objects && Array.isArray(data.objects)) {
513
- detections = data.objects;
514
- detectionSource = 'objects';
515
- } else if (data.results && Array.isArray(data.results)) {
516
- detections = data.results;
517
- detectionSource = 'results';
518
- } else {
519
- // Try to look one level deeper if no detections found
520
- for (const key in data) {
521
- if (typeof data[key] === 'object' && data[key] !== null) {
522
- if (Array.isArray(data[key])) {
523
- detections = data[key];
524
- detectionSource = key;
525
- break;
526
- } else {
527
- // Look one more level down
528
- for (const subKey in data[key]) {
529
- if (Array.isArray(data[key][subKey])) {
530
- detections = data[key][subKey];
531
- detectionSource = `${key}.${subKey}`;
532
- break;
533
- }
534
- }
535
- }
536
- }
537
- }
538
- }
539
-
540
- // Process each detection
541
- detections.forEach((detection, index) => {
542
- // Try to extract bounding box information
543
- let bbox = null;
544
- let label = null;
545
- let confidence = null;
546
- let distance = null;
547
-
548
- // Extract label/class
549
- if (detection.class !== undefined) {
550
- label = detection.class;
551
- } else {
552
- // Fallback to other common property names
553
- for (const key of ['label', 'name', 'category', 'className']) {
554
- if (detection[key] !== undefined) {
555
- label = detection[key];
556
- break;
557
- }
558
- }
559
- }
560
-
561
- // Default label if none found
562
- if (!label) label = `Object ${index + 1}`;
563
-
564
- // Extract confidence score if available
565
- for (const key of ['confidence', 'score', 'probability', 'conf']) {
566
- if (detection[key] !== undefined) {
567
- confidence = detection[key];
568
- break;
569
- }
570
- }
571
-
572
- // Extract distance - specifically look for distance_estimated first
573
- if (detection.distance_estimated !== undefined) {
574
- distance = detection.distance_estimated;
575
- } else {
576
- // Fallback to other common distance properties
577
- for (const key of ['distance', 'depth', 'z', 'dist', 'range']) {
578
- if (detection[key] !== undefined) {
579
- distance = detection[key];
580
- break;
581
- }
582
- }
583
- }
584
-
585
- // Look for bounding box in features
586
- if (detection.features &&
587
- detection.features.xmin !== undefined &&
588
- detection.features.ymin !== undefined &&
589
- detection.features.xmax !== undefined &&
590
- detection.features.ymax !== undefined) {
591
-
592
- bbox = {
593
- xmin: detection.features.xmin,
594
- ymin: detection.features.ymin,
595
- xmax: detection.features.xmax,
596
- ymax: detection.features.ymax
597
- };
598
- } else {
599
- // Recursively search for bbox-like properties
600
- function findBBox(obj, path = '') {
601
- if (!obj || typeof obj !== 'object') return null;
602
-
603
- // Check if this object looks like a bbox
604
- if ((obj.x !== undefined && obj.y !== undefined &&
605
- (obj.width !== undefined || obj.w !== undefined ||
606
- obj.height !== undefined || obj.h !== undefined)) ||
607
- (obj.xmin !== undefined && obj.ymin !== undefined &&
608
- obj.xmax !== undefined && obj.ymax !== undefined)) {
609
- return obj;
610
- }
611
-
612
- // Check if it's an array of 4 numbers (potential bbox)
613
- if (Array.isArray(obj) && obj.length === 4 &&
614
- obj.every(item => typeof item === 'number')) {
615
- return obj;
616
- }
617
-
618
- // Check common bbox property names
619
- for (const key of ['bbox', 'box', 'bounding_box', 'boundingBox']) {
620
- if (obj[key] !== undefined) {
621
- return obj[key];
622
- }
623
- }
624
-
625
- // Search nested properties
626
- for (const key in obj) {
627
- const result = findBBox(obj[key], path ? `${path}.${key}` : key);
628
- if (result) return result;
629
- }
630
-
631
- return null;
632
- }
633
-
634
- // Find bbox using recursive search as fallback
635
- bbox = findBBox(detection);
636
- }
637
-
638
- // If we found a bounding box, draw it
639
- if (bbox) {
640
- // Parse different bbox formats
641
- let x, y, width, height;
642
-
643
- if (Array.isArray(bbox)) {
644
- // Try to determine array format
645
- if (bbox.length === 4) {
646
- if (bbox[0] >= 0 && bbox[1] >= 0 && bbox[2] <= 1 && bbox[3] <= 1) {
647
- // Likely normalized [x1, y1, x2, y2]
648
- x = bbox[0] * processingWidth;
649
- y = bbox[1] * processingHeight;
650
- width = (bbox[2] - bbox[0]) * processingWidth;
651
- height = (bbox[3] - bbox[1]) * processingHeight;
652
- } else if (bbox[2] > bbox[0] && bbox[3] > bbox[1]) {
653
- // Likely [x1, y1, x2, y2]
654
- x = bbox[0];
655
- y = bbox[1];
656
- width = bbox[2] - bbox[0];
657
- height = bbox[3] - bbox[1];
658
- } else {
659
- // Assume [x, y, width, height]
660
- x = bbox[0];
661
- y = bbox[1];
662
- width = bbox[2];
663
- height = bbox[3];
664
- }
665
- }
666
- } else {
667
- // Object format with named properties
668
- if (bbox.x !== undefined && bbox.y !== undefined) {
669
- x = bbox.x;
670
- y = bbox.y;
671
- width = bbox.width || bbox.w || 0;
672
- height = bbox.height || bbox.h || 0;
673
- } else if (bbox.xmin !== undefined && bbox.ymin !== undefined) {
674
- x = bbox.xmin;
675
- y = bbox.ymin;
676
- width = (bbox.xmax || 0) - bbox.xmin;
677
- height = (bbox.ymax || 0) - bbox.ymin;
678
- }
679
- }
680
-
681
- // Validate coordinates
682
- if (x === undefined || y === undefined || width === undefined || height === undefined) {
683
- return;
684
- }
685
-
686
- // Check if we need to scale normalized coordinates (0-1)
687
- if (x >= 0 && x <= 1 && y >= 0 && y <= 1 && width >= 0 && width <= 1 && height >= 0 && height <= 1) {
688
- x = x * processingWidth;
689
- y = y * processingHeight;
690
- width = width * processingWidth;
691
- height = height * processingHeight;
692
- }
693
-
694
- // Generate a color based on the class name
695
- const hue = stringToHue(label);
696
- ctx.strokeStyle = `hsl(${hue}, 100%, 40%)`;
697
- ctx.fillStyle = `hsla(${hue}, 100%, 40%, 0.3)`;
698
-
699
- // Draw bounding box
700
- ctx.beginPath();
701
- ctx.rect(x, y, width, height);
702
- ctx.stroke();
703
- ctx.fill();
704
-
705
- // Format confidence value
706
- let confidenceText = "";
707
- if (confidence !== null && confidence !== undefined) {
708
- // Convert to percentage if it's a probability (0-1)
709
- if (confidence <= 1) {
710
- confidence = (confidence * 100).toFixed(0);
711
- } else {
712
- confidence = confidence.toFixed(0);
713
- }
714
- confidenceText = ` ${confidence}%`;
715
- }
716
-
717
- // Format distance value
718
- let distanceText = "";
719
- if (distance !== null && distance !== undefined) {
720
- distanceText = ` : ${distance.toFixed(2)} m`;
721
- }
722
-
723
- // Create label text
724
- const labelText = `${label}${confidenceText}${distanceText}`;
725
-
726
- // Measure text width
727
- const textWidth = ctx.measureText(labelText).width + 10;
728
-
729
- // Draw label background
730
- ctx.fillStyle = `hsl(${hue}, 100%, 40%)`;
731
- ctx.fillRect(x, y - 20, textWidth, 20);
732
-
733
- // Draw label text
734
- ctx.fillStyle = "white";
735
- ctx.fillText(labelText, x + 5, y - 5);
736
- }
737
- });
738
-
739
- } catch (error) {
740
- console.error('Error visualizing results:', error);
741
- debugOutput.style.display = 'block';
742
- debugOutput.textContent += `VISUALIZATION ERROR: ${error.message}\n`;
743
- debugOutput.textContent += `Error stack: ${error.stack}\n`;
744
- }
745
- }
746
-
747
- // Generate consistent hue for string
748
- function stringToHue(str) {
749
- let hash = 0;
750
- for (let i = 0; i < str.length; i++) {
751
- hash = str.charCodeAt(i) + ((hash << 5) - hash);
752
- }
753
- return hash % 360;
754
- }
755
-
756
- // Clear canvas
757
- function clearCanvas() {
758
- if (detectionCanvas.getContext) {
759
- ctx.clearRect(0, 0, detectionCanvas.width, detectionCanvas.height);
760
- }
761
- }
762
-
763
- // Show message function
764
- function showMessage(text, type) {
765
- message.textContent = text;
766
- message.className = '';
767
- message.classList.add(type);
768
- message.style.display = 'block';
769
-
770
- if (type === 'info') {
771
- setTimeout(() => {
772
- message.style.display = 'none';
773
- }, 3000);
774
- }
775
- }
776
- </script>
777
- </body>
 
778
  </html>
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>HTTP Image Upload Demo</title>
7
+ <style>
8
+ /* Reset and base styles */
9
+ * {
10
+ box-sizing: border-box;
11
+ margin: 0;
12
+ padding: 0;
13
+ }
14
+
15
+ body {
16
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
17
+ color: #333;
18
+ line-height: 1.6;
19
+ }
20
+
21
+ /* Demo section styling */
22
+ .execution-section {
23
+ max-width: 1200px;
24
+ margin: 0 auto;
25
+ padding: 2rem;
26
+ background-color: #f8f9fa;
27
+ border-radius: 8px;
28
+ box-shadow: 0 4px 6px rgba(0,0,0,0.1);
29
+ }
30
+
31
+ .section-title {
32
+ font-size: 2rem;
33
+ color: #384B70;
34
+ margin-bottom: 1rem;
35
+ padding-bottom: 0.5rem;
36
+ border-bottom: 2px solid #507687;
37
+ }
38
+
39
+ .demo-container {
40
+ display: flex;
41
+ flex-wrap: wrap;
42
+ gap: 2rem;
43
+ margin-top: 1.5rem;
44
+ }
45
+
46
+ .upload-container, .response-container {
47
+ flex: 1;
48
+ min-width: 300px;
49
+ padding: 1.5rem;
50
+ background-color: white;
51
+ border-radius: 8px;
52
+ box-shadow: 0 2px 4px rgba(0,0,0,0.05);
53
+ }
54
+
55
+ .container-title {
56
+ font-size: 1.5rem;
57
+ margin-bottom: 1rem;
58
+ color: #384B70;
59
+ }
60
+
61
+ /* Upload area styling */
62
+ .file-input-container {
63
+ border: 2px dashed #ccc;
64
+ border-radius: 5px;
65
+ padding: 2rem;
66
+ text-align: center;
67
+ margin-bottom: 1rem;
68
+ transition: all 0.3s ease;
69
+ }
70
+
71
+ .file-input-container:hover {
72
+ border-color: #507687;
73
+ background-color: #f8f9fa;
74
+ }
75
+
76
+ #fileInput {
77
+ display: none;
78
+ }
79
+
80
+ .file-label {
81
+ cursor: pointer;
82
+ display: flex;
83
+ flex-direction: column;
84
+ align-items: center;
85
+ gap: 0.5rem;
86
+ }
87
+
88
+ .file-icon {
89
+ font-size: 2.5rem;
90
+ color: #507687;
91
+ width: 64px;
92
+ height: 64px;
93
+ }
94
+ .file-placeholder {
95
+ max-width: 100%;
96
+ height: auto;
97
+ margin-top: 1rem;
98
+ border-radius: 4px;
99
+ display: none;
100
+ }
101
+
102
+ #sendButton {
103
+ background-color: #384B70;
104
+ color: white;
105
+ border: none;
106
+ border-radius: 4px;
107
+ padding: 0.75rem 1.5rem;
108
+ font-size: 1rem;
109
+ cursor: pointer;
110
+ transition: background-color 0.3s;
111
+ width: 100%;
112
+ margin-top: 1rem;
113
+ }
114
+
115
+ #sendButton:disabled {
116
+ background-color: #cccccc;
117
+ cursor: not-allowed;
118
+ }
119
+
120
+ #sendButton:hover:not(:disabled) {
121
+ background-color: #507687;
122
+ }
123
+
124
+ /* Response area styling */
125
+ .response-output {
126
+ height: 300px;
127
+ overflow-y: auto;
128
+ background-color: #f8f9fa;
129
+ border: 1px solid #ddd;
130
+ border-radius: 4px;
131
+ padding: 1rem;
132
+ font-family: monospace;
133
+ white-space: pre-wrap;
134
+ }
135
+
136
+ /* Tabs styling */
137
+ .tabs {
138
+ display: flex;
139
+ border-bottom: 1px solid #ddd;
140
+ margin-bottom: 1rem;
141
+ }
142
+
143
+ .tab-button {
144
+ padding: 0.5rem 1rem;
145
+ background-color: #f1f1f1;
146
+ border: none;
147
+ cursor: pointer;
148
+ transition: background-color 0.3s;
149
+ font-size: 1rem;
150
+ }
151
+
152
+ .tab-button.active {
153
+ background-color: #384B70;
154
+ color: white;
155
+ }
156
+
157
+ .tab-content {
158
+ display: none;
159
+ height: 300px;
160
+ }
161
+
162
+ .tab-content.active {
163
+ display: block;
164
+ }
165
+
166
+ /* Visualization area styling */
167
+ #visualizationContainer {
168
+ position: relative;
169
+ height: 100%;
170
+ overflow: auto;
171
+ background-color: #f8f9fa;
172
+ border: 1px solid #ddd;
173
+ border-radius: 4px;
174
+ }
175
+
176
+ .detection-canvas {
177
+ display: block;
178
+ margin: 0 auto;
179
+ }
180
+
181
+ /* Utilities */
182
+ #loading {
183
+ display: none;
184
+ margin-top: 1rem;
185
+ color: #384B70;
186
+ font-weight: bold;
187
+ text-align: center;
188
+ }
189
+
190
+ #message {
191
+ margin-top: 1rem;
192
+ padding: 0.75rem;
193
+ border-radius: 4px;
194
+ text-align: center;
195
+ display: none;
196
+ }
197
+
198
+ .error {
199
+ background-color: #ffebee;
200
+ color: #d32f2f;
201
+ }
202
+
203
+ .success {
204
+ background-color: #e8f5e9;
205
+ color: #388e3c;
206
+ }
207
+
208
+ .info {
209
+ font-size: 0.9rem;
210
+ color: #666;
211
+ margin-top: 0.5rem;
212
+ }
213
+
214
+ .stats {
215
+ margin-top: 1rem;
216
+ font-size: 0.9rem;
217
+ color: #666;
218
+ }
219
+
220
+ /* Debug output */
221
+ #debugOutput {
222
+ margin-top: 0.5rem;
223
+ font-size: 0.8rem;
224
+ color: #999;
225
+ border-top: 1px dashed #ddd;
226
+ padding-top: 0.5rem;
227
+ display: none;
228
+ }
229
+ </style>
230
+ </head>
231
+ <body>
232
+ <!-- Interactive Demo Section -->
233
+ <section class="execution-section">
234
+ <h2 class="section-title">Try It Yourself</h2>
235
+ <p>Upload an image and see the object detection and depth estimation results in real-time.</p>
236
+
237
+ <div class="demo-container">
238
+ <!-- Upload Container -->
239
+ <div class="upload-container">
240
+ <h3 class="container-title">Upload Image</h3>
241
+
242
+ <div class="file-input-container">
243
+ <label for="fileInput" class="file-label">
244
+ <img src="https://upload.wikimedia.org/wikipedia/commons/a/a1/Icons8_flat_folder.svg" class="file-icon"/>
245
+ <span>Click to select image</span>
246
+ <p class="info">PNG or JPEG, max 2MB</p>
247
+ </label>
248
+ <input type="file" accept="image/*" id="fileInput" />
249
+ <img id="imagePreview" class="file-placeholder" alt="Image preview" />
250
+ </div>
251
+
252
+ <button id="sendButton" disabled>Process Image</button>
253
+ <div id="loading">Processing your image...</div>
254
+ <div id="message"></div>
255
+
256
+ <div class="stats">
257
+ <div id="imageSize"></div>
258
+ <div id="processingTime"></div>
259
+ </div>
260
+
261
+ <div id="debugOutput"></div>
262
+ </div>
263
+
264
+ <!-- Response Container with Tabs -->
265
+ <div class="response-container">
266
+ <h3 class="container-title">Response</h3>
267
+
268
+ <div class="tabs">
269
+ <button class="tab-button active" data-tab="raw">Raw Output</button>
270
+ <button class="tab-button" data-tab="visual">Visual Output</button>
271
+ </div>
272
+
273
+ <!-- Raw Output Tab -->
274
+ <div id="rawTab" class="tab-content active">
275
+ <pre class="response-output" id="responseOutput">// Response will appear here after processing</pre>
276
+ </div>
277
+
278
+ <!-- Visual Output Tab -->
279
+ <div id="visualTab" class="tab-content">
280
+ <div id="visualizationContainer">
281
+ <canvas id="detectionCanvas" class="detection-canvas"></canvas>
282
+ </div>
283
+ </div>
284
+ </div>
285
+ </div>
286
+ </section>
287
+
288
+ <script>
289
+ // DOM Elements
290
+ const fileInput = document.getElementById('fileInput');
291
+ const imagePreview = document.getElementById('imagePreview');
292
+ const sendButton = document.getElementById('sendButton');
293
+ const loading = document.getElementById('loading');
294
+ const message = document.getElementById('message');
295
+ const responseOutput = document.getElementById('responseOutput');
296
+ const imageSizeInfo = document.getElementById('imageSize');
297
+ const processingTimeInfo = document.getElementById('processingTime');
298
+ const tabButtons = document.querySelectorAll('.tab-button');
299
+ const tabContents = document.querySelectorAll('.tab-content');
300
+ const detectionCanvas = document.getElementById('detectionCanvas');
301
+ const ctx = detectionCanvas.getContext('2d');
302
+ const debugOutput = document.getElementById('debugOutput');
303
+
304
+ // Enable debug mode (set to false in production)
305
+ const DEBUG = true;
306
+
307
+ // API endpoint URL
308
+ const API_URL = '/api/predict';
309
+
310
+ let imageFile = null;
311
+ let startTime = null;
312
+ let originalImage = null;
313
+ let processingWidth = 0;
314
+ let processingHeight = 0;
315
+ let responseData = null;
316
+
317
+ // Tab switching functionality
318
+ tabButtons.forEach(button => {
319
+ button.addEventListener('click', () => {
320
+ const tabName = button.getAttribute('data-tab');
321
+
322
+ // Update button states
323
+ tabButtons.forEach(btn => btn.classList.remove('active'));
324
+ button.classList.add('active');
325
+
326
+ // Update tab content visibility
327
+ tabContents.forEach(content => content.classList.remove('active'));
328
+ document.getElementById(tabName + 'Tab').classList.add('active');
329
+
330
+ // If switching to visual tab and we have data, ensure visualization is rendered
331
+ if (tabName === 'visual' && responseData && originalImage) {
332
+ visualizeResults(originalImage, responseData);
333
+ }
334
+ });
335
+ });
336
+
337
+ // Handle file input change
338
+ fileInput.addEventListener('change', (event) => {
339
+ const file = event.target.files[0];
340
+
341
+ // Clear previous selections
342
+ imageFile = null;
343
+ imagePreview.style.display = 'none';
344
+ sendButton.disabled = true;
345
+ originalImage = null;
346
+ responseData = null;
347
+
348
+ // Validate file
349
+ if (!file) return;
350
+
351
+ if (file.size > 2 * 1024 * 1024) {
352
+ showMessage('File size exceeds 2MB limit.', 'error');
353
+ return;
354
+ }
355
+
356
+ if (!['image/png', 'image/jpeg'].includes(file.type)) {
357
+ showMessage('Only PNG and JPEG formats are supported.', 'error');
358
+ return;
359
+ }
360
+
361
+ // Store file for upload
362
+ imageFile = file;
363
+
364
+ // Show image preview
365
+ const reader = new FileReader();
366
+ reader.onload = (e) => {
367
+ const image = new Image();
368
+ image.src = e.target.result;
369
+
370
+ image.onload = () => {
371
+ // Store original image for visualization
372
+ originalImage = image;
373
+
374
+ // Set preview
375
+ imagePreview.src = e.target.result;
376
+ imagePreview.style.display = 'block';
377
+
378
+ // Update image info
379
+ imageSizeInfo.textContent = `Original size: ${image.width}x${image.height} pixels`;
380
+
381
+ // Calculate processing dimensions (for visualization)
382
+ calculateProcessingDimensions(image.width, image.height);
383
+
384
+ // Enable send button
385
+ sendButton.disabled = false;
386
+ showMessage('Image ready to process.', 'info');
387
+ };
388
+ };
389
+ reader.readAsDataURL(file);
390
+ });
391
+
392
+ // Calculate dimensions for processing visualization
393
+ function calculateProcessingDimensions(width, height) {
394
+ const maxWidth = 640;
395
+ const maxHeight = 320;
396
+
397
+ // Calculate dimensions
398
+ if (width > height) {
399
+ if (width > maxWidth) {
400
+ height = Math.round((height * maxWidth) / width);
401
+ width = maxWidth;
402
+ }
403
+ } else {
404
+ if (height > maxHeight) {
405
+ width = Math.round((width * maxHeight) / height);
406
+ height = maxHeight;
407
+ }
408
+ }
409
+
410
+ // Store processing dimensions for visualization
411
+ processingWidth = width;
412
+ processingHeight = height;
413
+ }
414
+
415
+ // Handle send button click
416
+ sendButton.addEventListener('click', async () => {
417
+ if (!imageFile) {
418
+ showMessage('No image selected.', 'error');
419
+ return;
420
+ }
421
+
422
+ // Clear previous response
423
+ responseOutput.textContent = "// Processing...";
424
+ clearCanvas();
425
+ responseData = null;
426
+ debugOutput.style.display = 'none';
427
+
428
+ // Show loading state
429
+ loading.style.display = 'block';
430
+ message.style.display = 'none';
431
+
432
+ // Reset processing time
433
+ processingTimeInfo.textContent = '';
434
+
435
+ // Record start time
436
+ startTime = performance.now();
437
+
438
+ // Create form data for HTTP request
439
+ const formData = new FormData();
440
+ formData.append('file', imageFile);
441
+
442
+ try {
443
+ // Send HTTP request
444
+ const response = await fetch(API_URL, {
445
+ method: 'POST',
446
+ body: formData
447
+ });
448
+
449
+ // Handle response
450
+ if (!response.ok) {
451
+ const errorText = await response.text();
452
+ throw new Error(`HTTP error ${response.status}: ${errorText}`);
453
+ }
454
+
455
+ // Parse JSON response
456
+ const data = await response.json();
457
+ responseData = data;
458
+
459
+ // Calculate processing time
460
+ const endTime = performance.now();
461
+ const timeTaken = endTime - startTime;
462
+
463
+ // Format and display raw response
464
+ responseOutput.textContent = JSON.stringify(data, null, 2);
465
+ processingTimeInfo.textContent = `Processing time: ${timeTaken.toFixed(2)} ms`;
466
+
467
+ // Visualize the results
468
+ if (originalImage) {
469
+ visualizeResults(originalImage, data);
470
+ }
471
+
472
+ // Show success message
473
+ showMessage('Image processed successfully!', 'success');
474
+ } catch (error) {
475
+ console.error('Error processing image:', error);
476
+ showMessage(`Error: ${error.message}`, 'error');
477
+ responseOutput.textContent = `// Error: ${error.message}`;
478
+
479
+ if (DEBUG) {
480
+ debugOutput.style.display = 'block';
481
+ debugOutput.textContent = `Error: ${error.message}\n${error.stack || ''}`;
482
+ }
483
+ } finally {
484
+ loading.style.display = 'none';
485
+ }
486
+ });
487
+
488
+ // Visualize detection results
489
+ // Visualize detection results
490
+ function visualizeResults(image, data) {
491
+ try {
492
+ // Set canvas dimensions
493
+ detectionCanvas.width = processingWidth;
494
+ detectionCanvas.height = processingHeight;
495
+
496
+ // Draw the original image
497
+ ctx.drawImage(image, 0, 0, processingWidth, processingHeight);
498
+
499
+ // Set styles for bounding boxes
500
+ ctx.lineWidth = 3;
501
+ ctx.font = 'bold 14px Arial';
502
+
503
+ // Find detections (checking all common formats)
504
+ let detections = [];
505
+ let detectionSource = '';
506
+
507
+ if (data.detections && Array.isArray(data.detections)) {
508
+ detections = data.detections;
509
+ detectionSource = 'detections';
510
+ } else if (data.predictions && Array.isArray(data.predictions)) {
511
+ detections = data.predictions;
512
+ detectionSource = 'predictions';
513
+ } else if (data.objects && Array.isArray(data.objects)) {
514
+ detections = data.objects;
515
+ detectionSource = 'objects';
516
+ } else if (data.results && Array.isArray(data.results)) {
517
+ detections = data.results;
518
+ detectionSource = 'results';
519
+ } else {
520
+ // Try to look one level deeper if no detections found
521
+ for (const key in data) {
522
+ if (typeof data[key] === 'object' && data[key] !== null) {
523
+ if (Array.isArray(data[key])) {
524
+ detections = data[key];
525
+ detectionSource = key;
526
+ break;
527
+ } else {
528
+ // Look one more level down
529
+ for (const subKey in data[key]) {
530
+ if (Array.isArray(data[key][subKey])) {
531
+ detections = data[key][subKey];
532
+ detectionSource = `${key}.${subKey}`;
533
+ break;
534
+ }
535
+ }
536
+ }
537
+ }
538
+ }
539
+ }
540
+
541
+ // Process each detection
542
+ detections.forEach((detection, index) => {
543
+ // Try to extract bounding box information
544
+ let bbox = null;
545
+ let label = null;
546
+ let confidence = null;
547
+ let distance = null;
548
+
549
+ // Extract label/class
550
+ if (detection.class !== undefined) {
551
+ label = detection.class;
552
+ } else {
553
+ // Fallback to other common property names
554
+ for (const key of ['label', 'name', 'category', 'className']) {
555
+ if (detection[key] !== undefined) {
556
+ label = detection[key];
557
+ break;
558
+ }
559
+ }
560
+ }
561
+
562
+ // Default label if none found
563
+ if (!label) label = `Object ${index + 1}`;
564
+
565
+ // Extract confidence score if available
566
+ for (const key of ['confidence', 'score', 'probability', 'conf']) {
567
+ if (detection[key] !== undefined) {
568
+ confidence = detection[key];
569
+ break;
570
+ }
571
+ }
572
+
573
+ // Extract distance - specifically look for distance_estimated first
574
+ if (detection.distance_estimated !== undefined) {
575
+ distance = detection.distance_estimated;
576
+ } else {
577
+ // Fallback to other common distance properties
578
+ for (const key of ['distance', 'depth', 'z', 'dist', 'range']) {
579
+ if (detection[key] !== undefined) {
580
+ distance = detection[key];
581
+ break;
582
+ }
583
+ }
584
+ }
585
+
586
+ // Look for bounding box in features
587
+ if (detection.features &&
588
+ detection.features.xmin !== undefined &&
589
+ detection.features.ymin !== undefined &&
590
+ detection.features.xmax !== undefined &&
591
+ detection.features.ymax !== undefined) {
592
+
593
+ bbox = {
594
+ xmin: detection.features.xmin,
595
+ ymin: detection.features.ymin,
596
+ xmax: detection.features.xmax,
597
+ ymax: detection.features.ymax
598
+ };
599
+ } else {
600
+ // Recursively search for bbox-like properties
601
+ function findBBox(obj, path = '') {
602
+ if (!obj || typeof obj !== 'object') return null;
603
+
604
+ // Check if this object looks like a bbox
605
+ if ((obj.x !== undefined && obj.y !== undefined &&
606
+ (obj.width !== undefined || obj.w !== undefined ||
607
+ obj.height !== undefined || obj.h !== undefined)) ||
608
+ (obj.xmin !== undefined && obj.ymin !== undefined &&
609
+ obj.xmax !== undefined && obj.ymax !== undefined)) {
610
+ return obj;
611
+ }
612
+
613
+ // Check if it's an array of 4 numbers (potential bbox)
614
+ if (Array.isArray(obj) && obj.length === 4 &&
615
+ obj.every(item => typeof item === 'number')) {
616
+ return obj;
617
+ }
618
+
619
+ // Check common bbox property names
620
+ for (const key of ['bbox', 'box', 'bounding_box', 'boundingBox']) {
621
+ if (obj[key] !== undefined) {
622
+ return obj[key];
623
+ }
624
+ }
625
+
626
+ // Search nested properties
627
+ for (const key in obj) {
628
+ const result = findBBox(obj[key], path ? `${path}.${key}` : key);
629
+ if (result) return result;
630
+ }
631
+
632
+ return null;
633
+ }
634
+
635
+ // Find bbox using recursive search as fallback
636
+ bbox = findBBox(detection);
637
+ }
638
+
639
+ // If we found a bounding box, draw it
640
+ if (bbox) {
641
+ // Parse different bbox formats
642
+ let x, y, width, height;
643
+
644
+ if (Array.isArray(bbox)) {
645
+ // Try to determine array format
646
+ if (bbox.length === 4) {
647
+ if (bbox[0] >= 0 && bbox[1] >= 0 && bbox[2] <= 1 && bbox[3] <= 1) {
648
+ // Likely normalized [x1, y1, x2, y2]
649
+ x = bbox[0] * processingWidth;
650
+ y = bbox[1] * processingHeight;
651
+ width = (bbox[2] - bbox[0]) * processingWidth;
652
+ height = (bbox[3] - bbox[1]) * processingHeight;
653
+ } else if (bbox[2] > bbox[0] && bbox[3] > bbox[1]) {
654
+ // Likely [x1, y1, x2, y2]
655
+ x = bbox[0];
656
+ y = bbox[1];
657
+ width = bbox[2] - bbox[0];
658
+ height = bbox[3] - bbox[1];
659
+ } else {
660
+ // Assume [x, y, width, height]
661
+ x = bbox[0];
662
+ y = bbox[1];
663
+ width = bbox[2];
664
+ height = bbox[3];
665
+ }
666
+ }
667
+ } else {
668
+ // Object format with named properties
669
+ if (bbox.x !== undefined && bbox.y !== undefined) {
670
+ x = bbox.x;
671
+ y = bbox.y;
672
+ width = bbox.width || bbox.w || 0;
673
+ height = bbox.height || bbox.h || 0;
674
+ } else if (bbox.xmin !== undefined && bbox.ymin !== undefined) {
675
+ x = bbox.xmin;
676
+ y = bbox.ymin;
677
+ width = (bbox.xmax || 0) - bbox.xmin;
678
+ height = (bbox.ymax || 0) - bbox.ymin;
679
+ }
680
+ }
681
+
682
+ // Validate coordinates
683
+ if (x === undefined || y === undefined || width === undefined || height === undefined) {
684
+ return;
685
+ }
686
+
687
+ // Check if we need to scale normalized coordinates (0-1)
688
+ if (x >= 0 && x <= 1 && y >= 0 && y <= 1 && width >= 0 && width <= 1 && height >= 0 && height <= 1) {
689
+ x = x * processingWidth;
690
+ y = y * processingHeight;
691
+ width = width * processingWidth;
692
+ height = height * processingHeight;
693
+ }
694
+
695
+ // Generate a color based on the class name
696
+ const hue = stringToHue(label);
697
+ ctx.strokeStyle = `hsl(${hue}, 100%, 40%)`;
698
+ ctx.fillStyle = `hsla(${hue}, 100%, 40%, 0.3)`;
699
+
700
+ // Draw bounding box
701
+ ctx.beginPath();
702
+ ctx.rect(x, y, width, height);
703
+ ctx.stroke();
704
+ ctx.fill();
705
+
706
+ // Format confidence value
707
+ let confidenceText = "";
708
+ if (confidence !== null && confidence !== undefined) {
709
+ // Convert to percentage if it's a probability (0-1)
710
+ if (confidence <= 1) {
711
+ confidence = (confidence * 100).toFixed(0);
712
+ } else {
713
+ confidence = confidence.toFixed(0);
714
+ }
715
+ confidenceText = ` ${confidence}%`;
716
+ }
717
+
718
+ // Format distance value
719
+ let distanceText = "";
720
+ if (distance !== null && distance !== undefined) {
721
+ distanceText = ` : ${distance.toFixed(2)} m`;
722
+ }
723
+
724
+ // Create label text
725
+ const labelText = `${label}${confidenceText}${distanceText}`;
726
+
727
+ // Measure text width
728
+ const textWidth = ctx.measureText(labelText).width + 10;
729
+
730
+ // Draw label background
731
+ ctx.fillStyle = `hsl(${hue}, 100%, 40%)`;
732
+ ctx.fillRect(x, y - 20, textWidth, 20);
733
+
734
+ // Draw label text
735
+ ctx.fillStyle = "white";
736
+ ctx.fillText(labelText, x + 5, y - 5);
737
+ }
738
+ });
739
+
740
+ } catch (error) {
741
+ console.error('Error visualizing results:', error);
742
+ debugOutput.style.display = 'block';
743
+ debugOutput.textContent += `VISUALIZATION ERROR: ${error.message}\n`;
744
+ debugOutput.textContent += `Error stack: ${error.stack}\n`;
745
+ }
746
+ }
747
+
748
+ // Generate consistent hue for string
749
+ function stringToHue(str) {
750
+ let hash = 0;
751
+ for (let i = 0; i < str.length; i++) {
752
+ hash = str.charCodeAt(i) + ((hash << 5) - hash);
753
+ }
754
+ return hash % 360;
755
+ }
756
+
757
+ // Clear canvas
758
+ function clearCanvas() {
759
+ if (detectionCanvas.getContext) {
760
+ ctx.clearRect(0, 0, detectionCanvas.width, detectionCanvas.height);
761
+ }
762
+ }
763
+
764
+ // Show message function
765
+ function showMessage(text, type) {
766
+ message.textContent = text;
767
+ message.className = '';
768
+ message.classList.add(type);
769
+ message.style.display = 'block';
770
+
771
+ if (type === 'info') {
772
+ setTimeout(() => {
773
+ message.style.display = 'none';
774
+ }, 3000);
775
+ }
776
+ }
777
+ </script>
778
+ </body>
779
  </html>