Initial Commit
This commit is contained in:
485
templates/index.html
Normal file
485
templates/index.html
Normal file
@@ -0,0 +1,485 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Video Object Segmentation with SAM2</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.5em;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 1.1em;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.upload-section {
|
||||
margin-bottom: 30px;
|
||||
padding: 20px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 10px;
|
||||
border: 2px dashed #dee2e6;
|
||||
}
|
||||
|
||||
.upload-btn {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 15px 30px;
|
||||
font-size: 1.1em;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.upload-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.preview-section {
|
||||
margin: 30px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.preview-container {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.preview-image {
|
||||
max-width: 100%;
|
||||
max-height: 500px;
|
||||
border: 3px solid #667eea;
|
||||
border-radius: 10px;
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
.segmentation-section {
|
||||
margin: 30px 0;
|
||||
padding: 20px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.instructions {
|
||||
background: #e3f2fd;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
margin: 20px 0;
|
||||
border-left: 4px solid #2196f3;
|
||||
}
|
||||
|
||||
.btn {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 25px;
|
||||
font-size: 1em;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
margin: 5px;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||
}
|
||||
|
||||
.status {
|
||||
margin: 20px 0;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.status-success {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.status-error {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.status-info {
|
||||
background: #d1ecf1;
|
||||
color: #0c5460;
|
||||
}
|
||||
|
||||
.results-section {
|
||||
margin: 30px 0;
|
||||
}
|
||||
|
||||
.video-preview {
|
||||
max-width: 100%;
|
||||
border-radius: 10px;
|
||||
border: 3px solid #667eea;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.point-marker {
|
||||
position: absolute;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: red;
|
||||
border-radius: 50%;
|
||||
border: 2px solid white;
|
||||
transform: translate(-50%, -50%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: inline-block;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border: 5px solid #f3f3f3;
|
||||
border-top: 5px solid #667eea;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1>🎥 Video Object Segmentation with SAM2</h1>
|
||||
<p class="subtitle">Upload a video, click on objects, and let AI segment them out!</p>
|
||||
</header>
|
||||
|
||||
<div class="content">
|
||||
<div class="upload-section">
|
||||
<h2>📤 Upload Your Video</h2>
|
||||
<p>Supported formats: MP4, AVI, MOV, MKV</p>
|
||||
<input type="file" id="videoUpload" accept=".mp4,.avi,.mov,.mkv" style="display: none;">
|
||||
<button class="upload-btn" onclick="document.getElementById('videoUpload').click()">
|
||||
📁 Select Video File
|
||||
</button>
|
||||
<p id="fileInfo" style="margin-top: 15px;"></p>
|
||||
</div>
|
||||
|
||||
<div class="preview-section hidden" id="previewSection">
|
||||
<h2>🖼️ Preview & Select Object</h2>
|
||||
<div class="instructions">
|
||||
<strong>✨ Instructions:</strong> Click on the object you want to segment in the preview image below.
|
||||
You can click multiple points to help the AI better understand what to segment.
|
||||
</div>
|
||||
<div class="preview-container">
|
||||
<img id="previewImage" class="preview-image" alt="Video Preview">
|
||||
</div>
|
||||
<div style="margin-top: 20px;">
|
||||
<button class="btn" onclick="clearPoints()">🗑️ Clear Points</button>
|
||||
<button class="btn btn-secondary" onclick="segmentObject()">✂️ Segment Object</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="segmentation-section hidden" id="segmentationSection">
|
||||
<h2>⚙️ Processing</h2>
|
||||
<div class="status status-info" id="processingStatus">
|
||||
<div class="loading"></div>
|
||||
<p style="margin-top: 15px;">Segmenting your video... This may take a while depending on video length.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="results-section hidden" id="resultsSection">
|
||||
<h2>🎉 Results</h2>
|
||||
<div class="status status-success">
|
||||
<p>Segmentation completed successfully!</p>
|
||||
</div>
|
||||
<div style="text-align: center; margin: 20px 0;">
|
||||
<video id="resultVideo" class="video-preview" controls autoplay loop muted playsinline>
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
</div>
|
||||
<div style="text-align: center; margin: 20px 0;">
|
||||
<button class="btn" onclick="downloadResult()">💾 Download Result</button>
|
||||
<button class="btn btn-secondary" onclick="resetApp()">🔄 Start Over</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Global variables
|
||||
let currentFilename = '';
|
||||
let selectedPoints = [];
|
||||
let imageWidth, imageHeight;
|
||||
|
||||
// DOM elements
|
||||
const videoUpload = document.getElementById('videoUpload');
|
||||
const fileInfo = document.getElementById('fileInfo');
|
||||
const previewSection = document.getElementById('previewSection');
|
||||
const previewImage = document.getElementById('previewImage');
|
||||
const segmentationSection = document.getElementById('segmentationSection');
|
||||
const resultsSection = document.getElementById('resultsSection');
|
||||
const resultVideo = document.getElementById('resultVideo');
|
||||
const processingStatus = document.getElementById('processingStatus');
|
||||
|
||||
// Event listeners
|
||||
videoUpload.addEventListener('change', handleFileUpload);
|
||||
previewImage.addEventListener('click', handleImageClick);
|
||||
|
||||
function handleFileUpload(event) {
|
||||
const file = event.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
console.log('File selected:', file.name);
|
||||
console.log('File type:', file.type);
|
||||
console.log('File size:', file.size, 'bytes');
|
||||
|
||||
fileInfo.textContent = `Selected: ${file.name} (${(file.size / 1048576).toFixed(2)} MB)`;
|
||||
currentFilename = file.name;
|
||||
|
||||
// Upload file
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
console.log('Uploading file...');
|
||||
|
||||
fetch('/upload', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.error) {
|
||||
showError(data.error);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Upload response:', data);
|
||||
|
||||
// Show preview section
|
||||
previewSection.classList.remove('hidden');
|
||||
|
||||
if (data.preview) {
|
||||
// Display preview image
|
||||
previewImage.src = 'data:image/jpeg;base64,' + data.preview;
|
||||
|
||||
// Store image dimensions for point conversion
|
||||
previewImage.onload = function() {
|
||||
imageWidth = previewImage.naturalWidth;
|
||||
imageHeight = previewImage.naturalHeight;
|
||||
console.log('Preview image loaded:', imageWidth, 'x', imageHeight);
|
||||
};
|
||||
|
||||
previewImage.onerror = function() {
|
||||
console.error('Failed to load preview image');
|
||||
previewImage.src = '';
|
||||
previewImage.alt = 'Preview failed to load';
|
||||
};
|
||||
} else {
|
||||
console.log('No preview available');
|
||||
previewImage.src = '';
|
||||
previewImage.alt = 'No preview available';
|
||||
// You might want to show a message to the user here
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Upload error:', error);
|
||||
showError('Upload failed: ' + error.message);
|
||||
});
|
||||
}
|
||||
|
||||
function handleImageClick(event) {
|
||||
// Get click coordinates relative to the image
|
||||
const rect = previewImage.getBoundingClientRect();
|
||||
const x = event.clientX - rect.left;
|
||||
const y = event.clientY - rect.top;
|
||||
|
||||
// Convert to original image coordinates
|
||||
const scaleX = imageWidth / rect.width;
|
||||
const scaleY = imageHeight / rect.height;
|
||||
const originalX = Math.round(x * scaleX);
|
||||
const originalY = Math.round(y * scaleY);
|
||||
|
||||
// Add point
|
||||
selectedPoints.push([originalX, originalY]);
|
||||
|
||||
// Add visual marker
|
||||
addPointMarker(x, y);
|
||||
|
||||
console.log('Selected points:', selectedPoints);
|
||||
}
|
||||
|
||||
function addPointMarker(x, y) {
|
||||
const marker = document.createElement('div');
|
||||
marker.className = 'point-marker';
|
||||
marker.style.left = x + 'px';
|
||||
marker.style.top = y + 'px';
|
||||
previewImage.parentNode.appendChild(marker);
|
||||
}
|
||||
|
||||
function clearPoints() {
|
||||
selectedPoints = [];
|
||||
const markers = document.querySelectorAll('.point-marker');
|
||||
markers.forEach(marker => marker.remove());
|
||||
}
|
||||
|
||||
function segmentObject() {
|
||||
if (selectedPoints.length === 0) {
|
||||
alert('Please select at least one point on the object you want to segment.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!currentFilename) {
|
||||
showError('No video file selected');
|
||||
return;
|
||||
}
|
||||
|
||||
// Show processing section
|
||||
previewSection.classList.add('hidden');
|
||||
segmentationSection.classList.remove('hidden');
|
||||
|
||||
// Send segmentation request
|
||||
fetch('/segment', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
filename: currentFilename,
|
||||
points: selectedPoints
|
||||
})
|
||||
})
|
||||
.then(response => {
|
||||
console.log('Segmentation response status:', response.status);
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
console.log('Segmentation response data:', data);
|
||||
|
||||
if (data.error) {
|
||||
console.error('Segmentation error:', data.error);
|
||||
showError(data.error);
|
||||
return;
|
||||
}
|
||||
|
||||
// Show results section
|
||||
segmentationSection.classList.add('hidden');
|
||||
resultsSection.classList.remove('hidden');
|
||||
|
||||
// Display result video
|
||||
const videoUrl = '/segmented/' + data.output_filename;
|
||||
console.log('Setting video source to:', videoUrl);
|
||||
resultVideo.src = videoUrl;
|
||||
|
||||
// Add error handling for video
|
||||
resultVideo.onerror = function() {
|
||||
console.error('Video playback error');
|
||||
console.error('Video error code:', resultVideo.error ? resultVideo.error.code : 'unknown');
|
||||
showError('Could not load video. Error code: ' + (resultVideo.error ? resultVideo.error.code : 'unknown'));
|
||||
};
|
||||
|
||||
resultVideo.onloadeddata = function() {
|
||||
console.log('Video loaded successfully');
|
||||
console.log('Video duration:', resultVideo.duration, 'seconds');
|
||||
console.log('Video readyState:', resultVideo.readyState);
|
||||
};
|
||||
|
||||
resultVideo.load();
|
||||
console.log('Video load() called');
|
||||
|
||||
// Try to play the video
|
||||
setTimeout(() => {
|
||||
resultVideo.play().catch(e => {
|
||||
console.error('Autoplay failed:', e);
|
||||
// This is expected in some browsers without user interaction
|
||||
});
|
||||
}, 100);
|
||||
})
|
||||
.catch(error => {
|
||||
showError('Segmentation failed: ' + error.message);
|
||||
});
|
||||
}
|
||||
|
||||
function downloadResult() {
|
||||
if (!resultVideo.src) return;
|
||||
|
||||
const link = document.createElement('a');
|
||||
link.href = resultVideo.src;
|
||||
link.download = resultVideo.src.split('/').pop();
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
|
||||
function resetApp() {
|
||||
// Reset all state
|
||||
currentFilename = '';
|
||||
selectedPoints = [];
|
||||
fileInfo.textContent = '';
|
||||
videoUpload.value = '';
|
||||
|
||||
// Hide all sections
|
||||
previewSection.classList.add('hidden');
|
||||
segmentationSection.classList.add('hidden');
|
||||
resultsSection.classList.add('hidden');
|
||||
|
||||
// Clear markers and video
|
||||
const markers = document.querySelectorAll('.point-marker');
|
||||
markers.forEach(marker => marker.remove());
|
||||
previewImage.src = '';
|
||||
resultVideo.src = '';
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
processingStatus.className = 'status status-error';
|
||||
processingStatus.innerHTML = `<p>${message}</p>`;
|
||||
|
||||
// Show reset button
|
||||
setTimeout(() => {
|
||||
processingStatus.innerHTML += '<button class="btn" onclick="resetApp()" style="margin-top: 15px;">Try Again</button>';
|
||||
}, 1000);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user