485 lines
17 KiB
HTML
485 lines
17 KiB
HTML
<!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> |