Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Image Segmentation Demo</title> | |
<script src="https://cdn.tailwindcss.com"></script> | |
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script> | |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
<style> | |
.dropzone { | |
border: 2px dashed #9CA3AF; | |
border-radius: 0.5rem; | |
transition: all 0.3s ease; | |
} | |
.dropzone.active { | |
border-color: #3B82F6; | |
background-color: #EFF6FF; | |
} | |
.result-container { | |
transition: all 0.5s ease; | |
opacity: 0; | |
height: 0; | |
overflow: hidden; | |
} | |
.result-container.show { | |
opacity: 1; | |
height: auto; | |
} | |
.loading-spinner { | |
animation: spin 1s linear infinite; | |
} | |
@keyframes spin { | |
0% { transform: rotate(0deg); } | |
100% { transform: rotate(360deg); } | |
} | |
.image-container { | |
position: relative; | |
width: 100%; | |
height: 300px; | |
} | |
.image-wrapper { | |
position: relative; | |
width: 100%; | |
height: 100%; | |
overflow: hidden; | |
} | |
.image-wrapper img { | |
object-fit: contain; | |
width: 100%; | |
height: 100%; | |
} | |
.slider-container { | |
position: absolute; | |
bottom: 10px; | |
left: 50%; | |
transform: translateX(-50%); | |
background: rgba(0,0,0,0.5); | |
padding: 5px 10px; | |
border-radius: 20px; | |
color: white; | |
display: flex; | |
align-items: center; | |
gap: 10px; | |
} | |
.slider { | |
width: 150px; | |
} | |
</style> | |
</head> | |
<body class="bg-gray-50 min-h-screen"> | |
<div class="container mx-auto px-4 py-8"> | |
<header class="mb-8 text-center"> | |
<h1 class="text-3xl font-bold text-gray-800 mb-2">Image Segmentation Demo</h1> | |
<p class="text-gray-600">Upload an image to generate segmentation masks using our AI model</p> | |
</header> | |
<div class="max-w-4xl mx-auto bg-white rounded-xl shadow-md overflow-hidden p-6 mb-8"> | |
<div class="grid md:grid-cols-2 gap-8"> | |
<!-- Upload Section --> | |
<div> | |
<div id="dropzone" class="dropzone p-8 text-center cursor-pointer"> | |
<div class="flex flex-col items-center justify-center"> | |
<i class="fas fa-cloud-upload-alt text-4xl text-blue-500 mb-4"></i> | |
<h3 class="text-lg font-medium text-gray-700 mb-2">Drag & Drop your image here</h3> | |
<p class="text-gray-500 text-sm mb-4">or</p> | |
<label for="file-upload" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition cursor-pointer"> | |
Browse Files | |
</label> | |
<input id="file-upload" type="file" class="hidden" accept="image/*"> | |
<p class="text-gray-400 text-xs mt-4">Supports: JPG, PNG, WEBP (Max 5MB)</p> | |
</div> | |
</div> | |
<div id="preview-container" class="mt-4 hidden"> | |
<h3 class="text-sm font-medium text-gray-700 mb-2">Selected Image</h3> | |
<div class="image-container"> | |
<div class="image-wrapper bg-gray-100 rounded-lg"> | |
<img id="preview-image" src="" alt="Preview" class="hidden"> | |
</div> | |
</div> | |
<div class="flex justify-between mt-4"> | |
<button id="process-btn" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition"> | |
<i class="fas fa-magic mr-2"></i> Process Image | |
</button> | |
<button id="clear-btn" class="px-4 py-2 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300 transition"> | |
<i class="fas fa-trash-alt mr-2"></i> Clear | |
</button> | |
</div> | |
</div> | |
</div> | |
<!-- Results Section --> | |
<div> | |
<div id="loading-indicator" class="hidden"> | |
<div class="flex flex-col items-center justify-center h-full"> | |
<div class="loading-spinner border-4 border-blue-500 border-t-transparent rounded-full w-12 h-12 mb-4"></div> | |
<p class="text-gray-600">Processing your image...</p> | |
<p class="text-gray-400 text-sm">This may take a few moments</p> | |
</div> | |
</div> | |
<div id="result-container" class="result-container"> | |
<h3 class="text-lg font-medium text-gray-700 mb-4">Segmentation Results</h3> | |
<div class="mb-6"> | |
<div class="flex items-center justify-between mb-2"> | |
<h4 class="text-sm font-medium text-gray-700">Segmentation Mask</h4> | |
<div class="flex items-center"> | |
<button id="download-mask" class="text-blue-500 hover:text-blue-700 text-sm"> | |
<i class="fas fa-download mr-1"></i> Download | |
</button> | |
</div> | |
</div> | |
<div class="image-container"> | |
<div class="image-wrapper bg-gray-100 rounded-lg relative"> | |
<img id="mask-image" src="" alt="Segmentation Mask" class="hidden"> | |
<div class="slider-container hidden" id="mask-slider"> | |
<i class="fas fa-eye"></i> | |
<input type="range" min="0" max="100" value="50" class="slider" id="mask-opacity"> | |
<span id="mask-value">50%</span> | |
</div> | |
</div> | |
</div> | |
</div> | |
<div class="mb-6"> | |
<div class="flex items-center justify-between mb-2"> | |
<h4 class="text-sm font-medium text-gray-700">Overlay Comparison</h4> | |
<div class="flex items-center"> | |
<button id="download-overlay" class="text-blue-500 hover:text-blue-700 text-sm"> | |
<i class="fas fa-download mr-1"></i> Download | |
</button> | |
</div> | |
</div> | |
<div class="image-container"> | |
<div class="image-wrapper bg-gray-100 rounded-lg relative"> | |
<img id="overlay-image" src="" alt="Overlay" class="hidden"> | |
<div class="slider-container"> | |
<i class="fas fa-sliders-h"></i> | |
<input type="range" min="0" max="100" value="50" class="slider" id="overlay-opacity"> | |
<span id="overlay-value">50%</span> | |
</div> | |
</div> | |
</div> | |
</div> | |
<div class="bg-gray-50 p-4 rounded-lg"> | |
<h4 class="text-sm font-medium text-gray-700 mb-2">Segmentation Statistics</h4> | |
<div class="grid grid-cols-3 gap-4 text-center"> | |
<div class="bg-white p-2 rounded shadow"> | |
<p class="text-xs text-gray-500">Foreground Area</p> | |
<p id="foreground-area" class="font-bold">0 px²</p> | |
</div> | |
<div class="bg-white p-2 rounded shadow"> | |
<p class="text-xs text-gray-500">Background Area</p> | |
<p id="background-area" class="font-bold">0 px²</p> | |
</div> | |
<div class="bg-white p-2 rounded shadow"> | |
<p class="text-xs text-gray-500">Confidence</p> | |
<p id="confidence-score" class="font-bold">0%</p> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<div class="max-w-4xl mx-auto bg-white rounded-xl shadow-md overflow-hidden p-6"> | |
<h2 class="text-xl font-bold text-gray-800 mb-4">About This Model</h2> | |
<div class="grid md:grid-cols-2 gap-6"> | |
<div> | |
<h3 class="font-medium text-gray-700 mb-2">Model Details</h3> | |
<ul class="text-gray-600 space-y-2"> | |
<li class="flex items-start"> | |
<i class="fas fa-cog text-blue-500 mt-1 mr-2"></i> | |
<span>Architecture: Your Custom Segmentation Model</span> | |
</li> | |
<li class="flex items-start"> | |
<i class="fas fa-weight-hanging text-blue-500 mt-1 mr-2"></i> | |
<span>Model File: .pth format</span> | |
</li> | |
<li class="flex items-start"> | |
<i class="fas fa-tachometer-alt text-blue-500 mt-1 mr-2"></i> | |
<span>Inference Time: Varies by hardware</span> | |
</li> | |
<li class="flex items-start"> | |
<i class="fas fa-chart-line text-blue-500 mt-1 mr-2"></i> | |
<span>Custom Trained Model</span> | |
</li> | |
</ul> | |
</div> | |
<div> | |
<h3 class="font-medium text-gray-700 mb-2">Performance Metrics</h3> | |
<div class="h-48"> | |
<canvas id="metrics-chart"></canvas> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<script> | |
// DOM Elements | |
const dropzone = document.getElementById('dropzone'); | |
const fileUpload = document.getElementById('file-upload'); | |
const previewContainer = document.getElementById('preview-container'); | |
const previewImage = document.getElementById('preview-image'); | |
const processBtn = document.getElementById('process-btn'); | |
const clearBtn = document.getElementById('clear-btn'); | |
const loadingIndicator = document.getElementById('loading-indicator'); | |
const resultContainer = document.getElementById('result-container'); | |
const maskImage = document.getElementById('mask-image'); | |
const overlayImage = document.getElementById('overlay-image'); | |
const maskSlider = document.getElementById('mask-slider'); | |
const maskOpacity = document.getElementById('mask-opacity'); | |
const maskValue = document.getElementById('mask-value'); | |
const overlayOpacity = document.getElementById('overlay-opacity'); | |
const overlayValue = document.getElementById('overlay-value'); | |
const foregroundArea = document.getElementById('foreground-area'); | |
const backgroundArea = document.getElementById('background-area'); | |
const confidenceScore = document.getElementById('confidence-score'); | |
const downloadMask = document.getElementById('download-mask'); | |
const downloadOverlay = document.getElementById('download-overlay'); | |
// Event Listeners | |
dropzone.addEventListener('click', () => fileUpload.click()); | |
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { | |
dropzone.addEventListener(eventName, preventDefaults, false); | |
}); | |
function preventDefaults(e) { | |
e.preventDefault(); | |
e.stopPropagation(); | |
} | |
['dragenter', 'dragover'].forEach(eventName => { | |
dropzone.addEventListener(eventName, highlight, false); | |
}); | |
['dragleave', 'drop'].forEach(eventName => { | |
dropzone.addEventListener(eventName, unhighlight, false); | |
}); | |
function highlight() { | |
dropzone.classList.add('active'); | |
} | |
function unhighlight() { | |
dropzone.classList.remove('active'); | |
} | |
dropzone.addEventListener('drop', handleDrop, false); | |
fileUpload.addEventListener('change', handleFiles, false); | |
function handleDrop(e) { | |
const dt = e.dataTransfer; | |
const files = dt.files; | |
handleFiles({target: {files}}); | |
} | |
function handleFiles(e) { | |
const file = e.target.files[0]; | |
if (!file) return; | |
if (!file.type.match('image.*')) { | |
alert('Please select an image file (JPG, PNG, WEBP)'); | |
return; | |
} | |
if (file.size > 5 * 1024 * 1024) { | |
alert('File size exceeds 5MB limit'); | |
return; | |
} | |
const reader = new FileReader(); | |
reader.onload = function(e) { | |
previewImage.src = e.target.result; | |
previewImage.classList.remove('hidden'); | |
previewContainer.classList.remove('hidden'); | |
resultContainer.classList.remove('show'); | |
}; | |
reader.readAsDataURL(file); | |
} | |
processBtn.addEventListener('click', processImage); | |
clearBtn.addEventListener('click', clearAll); | |
function clearAll() { | |
fileUpload.value = ''; | |
previewImage.src = ''; | |
previewImage.classList.add('hidden'); | |
previewContainer.classList.add('hidden'); | |
maskImage.classList.add('hidden'); | |
overlayImage.classList.add('hidden'); | |
resultContainer.classList.remove('show'); | |
maskSlider.classList.add('hidden'); | |
} | |
maskOpacity.addEventListener('input', () => { | |
const value = maskOpacity.value; | |
maskValue.textContent = `${value}%`; | |
maskImage.style.opacity = value / 100; | |
}); | |
overlayOpacity.addEventListener('input', () => { | |
const value = overlayOpacity.value; | |
overlayValue.textContent = `${value}%`; | |
overlayImage.style.opacity = value / 100; | |
}); | |
async function processImage() { | |
if (!previewImage.src) { | |
alert('Please select an image first'); | |
return; | |
} | |
// Show loading indicator | |
loadingIndicator.classList.remove('hidden'); | |
resultContainer.classList.remove('show'); | |
try { | |
// Convert the image to a blob for upload | |
const blob = await fetch(previewImage.src).then(r => r.blob()); | |
// Create FormData to send the image | |
const formData = new FormData(); | |
formData.append('image', blob, 'uploaded_image.jpg'); | |
// 1. FIRST APPROACH: Using a Flask backend | |
// ----------------------------------------- | |
// You would need to create a Flask server that loads your model | |
// and processes the image. The endpoint would be something like: | |
// const response = await fetch('http://localhost:5000/process', { | |
// method: 'POST', | |
// body: formData | |
// }); | |
// 2. SECOND APPROACH: Using Pyodide to run Python in browser | |
// --------------------------------------------------------- | |
// This approach runs your model directly in the browser using WebAssembly | |
// You would need to: | |
// 1. Load Pyodide | |
// 2. Install required Python packages | |
// 3. Load your model | |
// 4. Process the image | |
// Here's a basic structure for the Pyodide approach: | |
/* | |
// Load Pyodide | |
let pyodide = await loadPyodide(); | |
// Install required packages | |
await pyodide.loadPackage(["numpy", "Pillow", "torch"]); | |
// Load your custom segmentation code | |
// You would need to convert your model to a format that can be loaded in Pyodide | |
// and include it in your project files | |
await pyodide.runPythonAsync(` | |
import your_segmentation_module | |
from your_segmentation_module import YourSegmentationModel | |
# Load your model (you would need to provide the path to your model file) | |
model = YourSegmentationModel() | |
model.load_state_dict(torch.load('path/to/your/model.pth')) | |
model.eval() | |
# Process image function | |
def process_image(image_data): | |
# Your image processing and segmentation logic here | |
# Return the mask and statistics | |
return mask, foreground_area, background_area, confidence | |
`); | |
// Process the image | |
const response = await pyodide.runPythonAsync(` | |
image_data = get_image_data() # You would need to implement this | |
mask, fg_area, bg_area, conf = process_image(image_data) | |
# Convert results to format that can be returned to JS | |
# ... | |
`); | |
*/ | |
// For this demo, we'll simulate a response | |
// In a real implementation, you would use one of the approaches above | |
const simulatedResponse = { | |
mask: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z/C/HgAGgwJ/lK3Q6wAAAABJRU5ErkJggg==", // Base64 encoded placeholder | |
foreground_area: 12345, | |
background_area: 45678, | |
confidence: 0.92 | |
}; | |
// Display the results | |
maskImage.src = `data:image/png;base64,${simulatedResponse.mask}`; | |
maskImage.classList.remove('hidden'); | |
maskSlider.classList.remove('hidden'); | |
overlayImage.src = previewImage.src; | |
overlayImage.classList.remove('hidden'); | |
// Update statistics | |
foregroundArea.textContent = `${simulatedResponse.foreground_area} px²`; | |
backgroundArea.textContent = `${simulatedResponse.background_area} px²`; | |
confidenceScore.textContent = `${Math.round(simulatedResponse.confidence * 100)}%`; | |
// Setup download buttons | |
downloadMask.onclick = () => downloadImage(maskImage.src, 'segmentation-mask.png'); | |
downloadOverlay.onclick = () => downloadImage(overlayImage.src, 'segmentation-overlay.png'); | |
// Show results | |
resultContainer.classList.add('show'); | |
} catch (error) { | |
console.error('Error:', error); | |
alert('Error processing image. Please try again.'); | |
} finally { | |
loadingIndicator.classList.add('hidden'); | |
} | |
} | |
function downloadImage(url, filename) { | |
const a = document.createElement('a'); | |
a.href = url; | |
a.download = filename; | |
document.body.appendChild(a); | |
a.click(); | |
document.body.removeChild(a); | |
} | |
// Initialize chart | |
document.addEventListener('DOMContentLoaded', function() { | |
const ctx = document.getElementById('metrics-chart').getContext('2d'); | |
const chart = new Chart(ctx, { | |
type: 'bar', | |
data: { | |
labels: ['IoU', 'Precision', 'Recall', 'Dice'], | |
datasets: [{ | |
label: 'Model Metrics', | |
data: [0.85, 0.88, 0.82, 0.87], // Replace with your model's actual metrics | |
backgroundColor: [ | |
'rgba(59, 130, 246, 0.7)', | |
'rgba(16, 185, 129, 0.7)', | |
'rgba(245, 158, 11, 0.7)', | |
'rgba(139, 92, 246, 0.7)' | |
], | |
borderColor: [ | |
'rgba(59, 130, 246, 1)', | |
'rgba(16, 185, 129, 1)', | |
'rgba(245, 158, 11, 1)', | |
'rgba(139, 92, 246, 1)' | |
], | |
borderWidth: 1 | |
}] | |
}, | |
options: { | |
responsive: true, | |
maintainAspectRatio: false, | |
scales: { | |
y: { | |
beginAtZero: true, | |
max: 1.0 | |
} | |
}, | |
plugins: { | |
legend: { | |
display: false | |
} | |
} | |
} | |
}); | |
}); | |
// MODEL INTEGRATION GUIDE | |
// ----------------------- | |
// To integrate your custom segmentation model, you have two main options: | |
// 1. Flask Backend Approach (Recommended for production) | |
// ------------------------------------------------------ | |
// - Create a Flask server that loads your model | |
// - The server should have an endpoint (e.g., /process) that: | |
// - Receives the image file | |
// - Processes it using your model | |
// - Returns the segmentation mask and statistics | |
// - In this HTML file, modify the processImage() function to call your Flask endpoint | |
// Example Flask server structure: | |
/* | |
from flask import Flask, request, jsonify | |
import your_segmentation_module | |
import base64 | |
import io | |
from PIL import Image | |
app = Flask(__name__) | |
# Load your model (replace with your actual model path) | |
model = your_segmentation_module.YourSegmentationModel() | |
model.load_state_dict(torch.load('path/to/your/model.pth')) | |
model.eval() | |
@app.route('/process', methods=['POST']) | |
def process_image(): | |
if 'image' not in request.files: | |
return jsonify({'error': 'No image provided'}), 400 | |
image_file = request.files['image'] | |
image = Image.open(image_file.stream) | |
# Process image with your model | |
mask, fg_area, bg_area, confidence = model.process(image) | |
# Convert mask to base64 | |
buffered = io.BytesIO() | |
mask.save(buffered, format="PNG") | |
mask_base64 = base64.b64encode(buffered.getvalue()).decode('utf-8') | |
return jsonify({ | |
'mask': mask_base64, | |
'foreground_area': fg_area, | |
'background_area': bg_area, | |
'confidence': confidence | |
}) | |
if __name__ == '__main__': | |
app.run(debug=True) | |
*/ | |
// 2. Pyodide Approach (For browser-only implementation) | |
// ---------------------------------------------------- | |
// - This runs Python directly in the browser using WebAssembly | |
// - Limitations: Larger initial load time, limited package support | |
// - Steps: | |
// a. Include Pyodide in your project | |
// b. Convert your model to a format that can be loaded in Pyodide | |
// c. Modify the processImage() function to use Pyodide | |
// Model Path Configuration: | |
// ------------------------- | |
// Wherever you see 'path/to/your/model.pth' in the code comments, | |
// replace it with the actual path to your trained model file. | |
// This could be: | |
// - A relative path from your Flask server's root directory | |
// - An absolute path on your server | |
// - A URL if hosting the model file online | |
</script> | |
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=vishnumeher/amb" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
</html> |