Spaces:
Sleeping
Sleeping
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>HTTP Image Upload Demo</title> | |
<style> | |
/* Reset and base styles */ | |
* { | |
box-sizing: border-box; | |
margin: 0; | |
padding: 0; | |
} | |
body { | |
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
color: #333; | |
line-height: 1.6; | |
} | |
/* Demo section styling */ | |
.execution-section { | |
max-width: 1200px; | |
margin: 0 auto; | |
padding: 2rem; | |
background-color: #f8f9fa; | |
border-radius: 8px; | |
box-shadow: 0 4px 6px rgba(0,0,0,0.1); | |
} | |
.section-title { | |
font-size: 2rem; | |
color: #384B70; | |
margin-bottom: 1rem; | |
padding-bottom: 0.5rem; | |
border-bottom: 2px solid #507687; | |
} | |
.demo-container { | |
display: flex; | |
flex-wrap: wrap; | |
gap: 2rem; | |
margin-top: 1.5rem; | |
} | |
.upload-container, .response-container { | |
flex: 1; | |
min-width: 300px; | |
padding: 1.5rem; | |
background-color: white; | |
border-radius: 8px; | |
box-shadow: 0 2px 4px rgba(0,0,0,0.05); | |
} | |
.container-title { | |
font-size: 1.5rem; | |
margin-bottom: 1rem; | |
color: #384B70; | |
} | |
/* Upload area styling */ | |
.file-input-container { | |
border: 2px dashed #ccc; | |
border-radius: 5px; | |
padding: 2rem; | |
text-align: center; | |
margin-bottom: 1rem; | |
transition: all 0.3s ease; | |
} | |
.file-input-container:hover { | |
border-color: #507687; | |
background-color: #f8f9fa; | |
} | |
#fileInput { | |
display: none; | |
} | |
.file-label { | |
cursor: pointer; | |
display: flex; | |
flex-direction: column; | |
align-items: center; | |
gap: 0.5rem; | |
} | |
.file-icon { | |
font-size: 2.5rem; | |
color: #507687; | |
width: 64px; | |
height: 64px; | |
} | |
.file-placeholder { | |
max-width: 100%; | |
height: auto; | |
margin-top: 1rem; | |
border-radius: 4px; | |
display: none; | |
} | |
#sendButton { | |
background-color: #384B70; | |
color: white; | |
border: none; | |
border-radius: 4px; | |
padding: 0.75rem 1.5rem; | |
font-size: 1rem; | |
cursor: pointer; | |
transition: background-color 0.3s; | |
width: 100%; | |
margin-top: 1rem; | |
} | |
#sendButton:disabled { | |
background-color: #cccccc; | |
cursor: not-allowed; | |
} | |
#sendButton:hover:not(:disabled) { | |
background-color: #507687; | |
} | |
/* Response area styling */ | |
.response-output { | |
height: 300px; | |
overflow-y: auto; | |
background-color: #f8f9fa; | |
border: 1px solid #ddd; | |
border-radius: 4px; | |
padding: 1rem; | |
font-family: monospace; | |
white-space: pre-wrap; | |
} | |
/* Tabs styling */ | |
.tabs { | |
display: flex; | |
border-bottom: 1px solid #ddd; | |
margin-bottom: 1rem; | |
} | |
.tab-button { | |
padding: 0.5rem 1rem; | |
background-color: #f1f1f1; | |
border: none; | |
cursor: pointer; | |
transition: background-color 0.3s; | |
font-size: 1rem; | |
} | |
.tab-button.active { | |
background-color: #384B70; | |
color: white; | |
} | |
.tab-content { | |
display: none; | |
height: 300px; | |
} | |
.tab-content.active { | |
display: block; | |
} | |
/* Visualization area styling */ | |
#visualizationContainer { | |
position: relative; | |
height: 100%; | |
overflow: auto; | |
background-color: #f8f9fa; | |
border: 1px solid #ddd; | |
border-radius: 4px; | |
} | |
.detection-canvas { | |
display: block; | |
margin: 0 auto; | |
} | |
/* Utilities */ | |
#loading { | |
display: none; | |
margin-top: 1rem; | |
color: #384B70; | |
font-weight: bold; | |
text-align: center; | |
} | |
#message { | |
margin-top: 1rem; | |
padding: 0.75rem; | |
border-radius: 4px; | |
text-align: center; | |
display: none; | |
} | |
.error { | |
background-color: #ffebee; | |
color: #d32f2f; | |
} | |
.success { | |
background-color: #e8f5e9; | |
color: #388e3c; | |
} | |
.info { | |
font-size: 0.9rem; | |
color: #666; | |
margin-top: 0.5rem; | |
} | |
.stats { | |
margin-top: 1rem; | |
font-size: 0.9rem; | |
color: #666; | |
} | |
/* Debug output */ | |
#debugOutput { | |
margin-top: 0.5rem; | |
font-size: 0.8rem; | |
color: #999; | |
border-top: 1px dashed #ddd; | |
padding-top: 0.5rem; | |
display: none; | |
} | |
</style> | |
</head> | |
<body> | |
<!-- Interactive Demo Section --> | |
<section class="execution-section"> | |
<h2 class="section-title">Try It Yourself</h2> | |
<p>Upload an image and see the object detection and depth estimation results in real-time.</p> | |
<div class="demo-container"> | |
<!-- Upload Container --> | |
<div class="upload-container"> | |
<h3 class="container-title">Upload Image</h3> | |
<div class="file-input-container"> | |
<label for="fileInput" class="file-label"> | |
<img src="https://upload.wikimedia.org/wikipedia/commons/a/a1/Icons8_flat_folder.svg" class="file-icon"/> | |
<span>Click to select image</span> | |
<p class="info">PNG or JPEG, max 2MB</p> | |
</label> | |
<input type="file" accept="image/*" id="fileInput" /> | |
<img id="imagePreview" class="file-placeholder" alt="Image preview" /> | |
</div> | |
<button id="sendButton" disabled>Process Image</button> | |
<div id="loading">Processing your image...</div> | |
<div id="message"></div> | |
<div class="stats"> | |
<div id="imageSize"></div> | |
<div id="processingTime"></div> | |
</div> | |
<div id="debugOutput"></div> | |
</div> | |
<!-- Response Container with Tabs --> | |
<div class="response-container"> | |
<h3 class="container-title">Response</h3> | |
<div class="tabs"> | |
<button class="tab-button active" data-tab="raw">Raw Output</button> | |
<button class="tab-button" data-tab="visual">Visual Output</button> | |
</div> | |
<!-- Raw Output Tab --> | |
<div id="rawTab" class="tab-content active"> | |
<pre class="response-output" id="responseOutput">// Response will appear here after processing</pre> | |
</div> | |
<!-- Visual Output Tab --> | |
<div id="visualTab" class="tab-content"> | |
<div id="visualizationContainer"> | |
<canvas id="detectionCanvas" class="detection-canvas"></canvas> | |
</div> | |
</div> | |
</div> | |
</div> | |
</section> | |
<script> | |
// DOM Elements | |
const fileInput = document.getElementById('fileInput'); | |
const imagePreview = document.getElementById('imagePreview'); | |
const sendButton = document.getElementById('sendButton'); | |
const loading = document.getElementById('loading'); | |
const message = document.getElementById('message'); | |
const responseOutput = document.getElementById('responseOutput'); | |
const imageSizeInfo = document.getElementById('imageSize'); | |
const processingTimeInfo = document.getElementById('processingTime'); | |
const tabButtons = document.querySelectorAll('.tab-button'); | |
const tabContents = document.querySelectorAll('.tab-content'); | |
const detectionCanvas = document.getElementById('detectionCanvas'); | |
const ctx = detectionCanvas.getContext('2d'); | |
const debugOutput = document.getElementById('debugOutput'); | |
// Enable debug mode (set to false in production) | |
const DEBUG = true; | |
// API endpoint URL | |
const API_URL = '/api/predict'; | |
let imageFile = null; | |
let startTime = null; | |
let originalImage = null; | |
let processingWidth = 0; | |
let processingHeight = 0; | |
let responseData = null; | |
// Tab switching functionality | |
tabButtons.forEach(button => { | |
button.addEventListener('click', () => { | |
const tabName = button.getAttribute('data-tab'); | |
// Update button states | |
tabButtons.forEach(btn => btn.classList.remove('active')); | |
button.classList.add('active'); | |
// Update tab content visibility | |
tabContents.forEach(content => content.classList.remove('active')); | |
document.getElementById(tabName + 'Tab').classList.add('active'); | |
// If switching to visual tab and we have data, ensure visualization is rendered | |
if (tabName === 'visual' && responseData && originalImage) { | |
visualizeResults(originalImage, responseData); | |
} | |
}); | |
}); | |
// Handle file input change | |
fileInput.addEventListener('change', (event) => { | |
const file = event.target.files[0]; | |
// Clear previous selections | |
imageFile = null; | |
imagePreview.style.display = 'none'; | |
sendButton.disabled = true; | |
originalImage = null; | |
responseData = null; | |
// Validate file | |
if (!file) return; | |
if (file.size > 2 * 1024 * 1024) { | |
showMessage('File size exceeds 2MB limit.', 'error'); | |
return; | |
} | |
if (!['image/png', 'image/jpeg'].includes(file.type)) { | |
showMessage('Only PNG and JPEG formats are supported.', 'error'); | |
return; | |
} | |
// Store file for upload | |
imageFile = file; | |
// Show image preview | |
const reader = new FileReader(); | |
reader.onload = (e) => { | |
const image = new Image(); | |
image.src = e.target.result; | |
image.onload = () => { | |
// Store original image for visualization | |
originalImage = image; | |
// Set preview | |
imagePreview.src = e.target.result; | |
imagePreview.style.display = 'block'; | |
// Update image info | |
imageSizeInfo.textContent = `Original size: ${image.width}x${image.height} pixels`; | |
// Resize image for processing | |
resizeImage(image, file.type); | |
// Enable send button | |
sendButton.disabled = false; | |
showMessage('Image ready to process.', 'info'); | |
}; | |
}; | |
reader.readAsDataURL(file); | |
}); | |
// Resize image function | |
function resizeImage(image, fileType) { | |
const canvas = document.createElement('canvas'); | |
const maxWidth = 640; | |
const maxHeight = 320; | |
let width = image.width; | |
let height = image.height; | |
// Calculate dimensions | |
if (width > height) { | |
if (width > maxWidth) { | |
height = Math.round((height * maxWidth) / width); | |
width = maxWidth; | |
} | |
} else { | |
if (height > maxHeight) { | |
width = Math.round((width * maxHeight) / height); | |
height = maxHeight; | |
} | |
} | |
// Store processing dimensions for visualization | |
processingWidth = width; | |
processingHeight = height; | |
// Set canvas dimensions and draw image | |
canvas.width = width; | |
canvas.height = height; | |
const ctx = canvas.getContext('2d'); | |
ctx.drawImage(image, 0, 0, width, height); | |
// For API calls, we don't need to convert to binary | |
// but we keep this method to ensure dimensions are correctly calculated | |
} | |
// Handle send button click | |
sendButton.addEventListener('click', async () => { | |
if (!imageFile) { | |
showMessage('No image selected.', 'error'); | |
return; | |
} | |
// Clear previous response | |
responseOutput.textContent = "// Processing..."; | |
clearCanvas(); | |
responseData = null; | |
debugOutput.style.display = 'none'; | |
// Show loading state | |
loading.style.display = 'block'; | |
message.style.display = 'none'; | |
// Reset processing time | |
processingTimeInfo.textContent = ''; | |
// Record start time | |
startTime = performance.now(); | |
// Create form data for HTTP request | |
const formData = new FormData(); | |
formData.append('file', imageFile); | |
try { | |
// Send HTTP request | |
const response = await fetch(API_URL, { | |
method: 'POST', | |
body: formData | |
}); | |
// Handle response | |
if (!response.ok) { | |
const errorText = await response.text(); | |
throw new Error(`HTTP error ${response.status}: ${errorText}`); | |
} | |
// Parse JSON response | |
const data = await response.json(); | |
responseData = data; | |
// Calculate processing time | |
const endTime = performance.now(); | |
const timeTaken = endTime - startTime; | |
// Format and display raw response | |
responseOutput.textContent = JSON.stringify(data, null, 2); | |
processingTimeInfo.textContent = `Processing time: ${timeTaken.toFixed(2)} ms`; | |
// Visualize the results | |
if (originalImage) { | |
visualizeResults(originalImage, data); | |
} | |
// Show success message | |
showMessage('Image processed successfully!', 'success'); | |
} catch (error) { | |
console.error('Error processing image:', error); | |
showMessage(`Error: ${error.message}`, 'error'); | |
responseOutput.textContent = `// Error: ${error.message}`; | |
if (DEBUG) { | |
debugOutput.style.display = 'block'; | |
debugOutput.textContent = `Error: ${error.message}\n${error.stack || ''}`; | |
} | |
} finally { | |
loading.style.display = 'none'; | |
} | |
}); | |
// Visualize detection results | |
function visualizeResults(image, data) { | |
try { | |
// Set canvas dimensions | |
detectionCanvas.width = processingWidth; | |
detectionCanvas.height = processingHeight; | |
// Draw the resized original image | |
ctx.drawImage(image, 0, 0, processingWidth, processingHeight); | |
// Set styles for bounding boxes | |
ctx.lineWidth = 3; | |
ctx.font = 'bold 14px Arial'; | |
// Find detections from various possible keys | |
let detections = []; | |
if (data.detections && Array.isArray(data.detections)) { | |
detections = data.detections; | |
} else if (data.predictions && Array.isArray(data.predictions)) { | |
detections = data.predictions; | |
} else if (data.objects && Array.isArray(data.objects)) { | |
detections = data.objects; | |
} else if (data.results && Array.isArray(data.results)) { | |
detections = data.results; | |
} else { | |
// Try to look one level deeper if no detections found | |
for (const key in data) { | |
if (typeof data[key] === 'object' && data[key] !== null) { | |
if (Array.isArray(data[key])) { | |
detections = data[key]; | |
break; | |
} else { | |
for (const subKey in data[key]) { | |
if (Array.isArray(data[key][subKey])) { | |
detections = data[key][subKey]; | |
break; | |
} | |
} | |
} | |
} | |
} | |
} | |
// Scaling factors based on original image vs resized processing dimensions | |
const scaleX = processingWidth / image.width; | |
const scaleY = processingHeight / image.height; | |
// Process each detection | |
detections.forEach((detection, index) => { | |
let bbox = null; | |
let label = null; | |
let confidence = null; | |
let distance = null; | |
// Extract label | |
if (detection.class !== undefined) { | |
label = detection.class; | |
} else { | |
for (const key of ['label', 'name', 'category', 'className']) { | |
if (detection[key] !== undefined) { | |
label = detection[key]; | |
break; | |
} | |
} | |
} | |
if (!label) label = `Object ${index + 1}`; | |
// Extract confidence score | |
for (const key of ['confidence', 'score', 'probability', 'conf']) { | |
if (detection[key] !== undefined) { | |
confidence = detection[key]; | |
break; | |
} | |
} | |
// Extract distance (using 'distance_estimated' first) | |
if (detection.distance_estimated !== undefined) { | |
distance = detection.distance_estimated; | |
} else { | |
for (const key of ['distance', 'depth', 'z', 'dist', 'range']) { | |
if (detection[key] !== undefined) { | |
distance = detection[key]; | |
break; | |
} | |
} | |
} | |
// Attempt to get bounding box coordinates | |
if (detection.features && | |
detection.features.xmin !== undefined && | |
detection.features.ymin !== undefined && | |
detection.features.xmax !== undefined && | |
detection.features.ymax !== undefined) { | |
bbox = { | |
xmin: detection.features.xmin, | |
ymin: detection.features.ymin, | |
xmax: detection.features.xmax, | |
ymax: detection.features.ymax | |
}; | |
} else { | |
// Recursive search for bbox-like properties | |
function findBBox(obj) { | |
if (!obj || typeof obj !== 'object') return null; | |
if ((obj.x !== undefined && obj.y !== undefined && | |
(obj.width !== undefined || obj.w !== undefined || | |
obj.height !== undefined || obj.h !== undefined)) || | |
(obj.xmin !== undefined && obj.ymin !== undefined && | |
obj.xmax !== undefined && obj.ymax !== undefined)) { | |
return obj; | |
} | |
if (Array.isArray(obj) && obj.length === 4 && | |
obj.every(item => typeof item === 'number')) { | |
return obj; | |
} | |
for (const key of ['bbox', 'box', 'bounding_box', 'boundingBox']) { | |
if (obj[key] !== undefined) { | |
return obj[key]; | |
} | |
} | |
for (const key in obj) { | |
const result = findBBox(obj[key]); | |
if (result) return result; | |
} | |
return null; | |
} | |
bbox = findBBox(detection); | |
} | |
// If bounding box found, process and draw it | |
if (bbox) { | |
let x, y, width, height; | |
if (Array.isArray(bbox)) { | |
if (bbox.length === 4) { | |
// Check if values are normalized (all between 0 and 1) | |
const isNormalized = bbox.every(val => val >= 0 && val <= 1); | |
if (isNormalized) { | |
x = bbox[0] * processingWidth; | |
y = bbox[1] * processingHeight; | |
width = (bbox[2] - bbox[0]) * processingWidth; | |
height = (bbox[3] - bbox[1]) * processingHeight; | |
} else if (bbox[2] > bbox[0] && bbox[3] > bbox[1]) { | |
// Absolute coordinates | |
x = bbox[0] * scaleX; | |
y = bbox[1] * scaleY; | |
width = (bbox[2] - bbox[0]) * scaleX; | |
height = (bbox[3] - bbox[1]) * scaleY; | |
} else { | |
// Format assumed to be [x, y, width, height] | |
x = bbox[0] * scaleX; | |
y = bbox[1] * scaleY; | |
width = bbox[2] * scaleX; | |
height = bbox[3] * scaleY; | |
} | |
} | |
} else { | |
// Object format handling | |
if (bbox.x !== undefined && bbox.y !== undefined) { | |
// x,y,width,height format | |
x = bbox.x; | |
y = bbox.y; | |
width = bbox.width || bbox.w || 0; | |
height = bbox.height || bbox.h || 0; | |
// Check if normalized | |
if (x <= 1 && y <= 1 && width <= 1 && height <= 1) { | |
x *= processingWidth; | |
y *= processingHeight; | |
width *= processingWidth; | |
height *= processingHeight; | |
} else { | |
// Assume coordinates are based on the original image | |
x *= scaleX; | |
y *= scaleY; | |
width *= scaleX; | |
height *= scaleY; | |
} | |
} else if (bbox.xmin !== undefined && bbox.ymin !== undefined) { | |
x = bbox.xmin; | |
y = bbox.ymin; | |
width = (bbox.xmax || 0) - bbox.xmin; | |
height = (bbox.ymax || 0) - bbox.ymin; | |
if (x <= 1 && y <= 1 && bbox.xmax <= 1 && bbox.ymax <= 1) { | |
x *= processingWidth; | |
y *= processingHeight; | |
width *= processingWidth; | |
height *= processingHeight; | |
} else { | |
x *= scaleX; | |
y *= scaleY; | |
width *= scaleX; | |
height *= scaleY; | |
} | |
} | |
} | |
// Draw the bounding box if coordinates are valid | |
if (x !== undefined && y !== undefined && | |
width !== undefined && height !== undefined && | |
width > 0 && height > 0) { | |
// Generate a color based on the label | |
const hue = stringToHue(label); | |
ctx.strokeStyle = `hsl(${hue}, 100%, 40%)`; | |
ctx.fillStyle = `hsla(${hue}, 100%, 40%, 0.3)`; | |
// Draw the bounding box rectangle | |
ctx.beginPath(); | |
ctx.rect(x, y, width, height); | |
ctx.stroke(); | |
ctx.fill(); | |
// Format and display confidence value if available | |
let confidenceText = ""; | |
if (confidence !== null && confidence !== undefined) { | |
if (confidence <= 1) { | |
confidenceText = ` ${(confidence * 100).toFixed(0)}%`; | |
} else { | |
confidenceText = ` ${confidence.toFixed(0)}%`; | |
} | |
} | |
// Format distance if available | |
let distanceText = ""; | |
if (distance !== null && distance !== undefined) { | |
distanceText = ` : ${distance.toFixed(2)} m`; | |
} | |
// Prepare the label text | |
const labelText = `${label}${confidenceText}${distanceText}`; | |
const textWidth = ctx.measureText(labelText).width + 10; | |
// Draw label background | |
ctx.fillStyle = `hsl(${hue}, 100%, 40%)`; | |
ctx.fillRect(x, y - 20, textWidth, 20); | |
// Draw label text | |
ctx.fillStyle = "white"; | |
ctx.fillText(labelText, x + 5, y - 5); | |
} | |
} | |
}); | |
} catch (error) { | |
console.error('Error visualizing results:', error); | |
// Optional: Display debug information if needed | |
debugOutput.style.display = 'block'; | |
debugOutput.textContent += `VISUALIZATION ERROR: ${error.message}\n${error.stack}\n`; | |
} | |
} | |
// Generate consistent hue for string | |
function stringToHue(str) { | |
let hash = 0; | |
for (let i = 0; i < str.length; i++) { | |
hash = str.charCodeAt(i) + ((hash << 5) - hash); | |
} | |
return hash % 360; | |
} | |
// Clear canvas | |
function clearCanvas() { | |
if (detectionCanvas.getContext) { | |
ctx.clearRect(0, 0, detectionCanvas.width, detectionCanvas.height); | |
} | |
} | |
// Show message function | |
function showMessage(text, type) { | |
message.textContent = text; | |
message.className = ''; | |
message.classList.add(type); | |
message.style.display = 'block'; | |
if (type === 'info') { | |
setTimeout(() => { | |
message.style.display = 'none'; | |
}, 3000); | |
} | |
} | |
</script> | |
</body> | |
</html> |