Spaces:
Running
Running
| <html lang="ru"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>SAM2 Box Prompt Demo</title> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| min-height: 100vh; | |
| padding: 20px; | |
| } | |
| .container { | |
| max-width: 1400px; | |
| margin: 0 auto; | |
| } | |
| h1 { | |
| color: white; | |
| text-align: center; | |
| margin-bottom: 10px; | |
| font-size: 2.5em; | |
| text-shadow: 2px 2px 4px rgba(0,0,0,0.3); | |
| } | |
| .subtitle { | |
| color: rgba(255,255,255,0.9); | |
| text-align: center; | |
| margin-bottom: 30px; | |
| font-size: 1.1em; | |
| } | |
| .panel { | |
| background: white; | |
| border-radius: 16px; | |
| padding: 30px; | |
| box-shadow: 0 20px 60px rgba(0,0,0,0.3); | |
| margin-bottom: 20px; | |
| } | |
| .upload-section { | |
| text-align: center; | |
| padding: 40px; | |
| border: 3px dashed #667eea; | |
| border-radius: 12px; | |
| background: #f8f9ff; | |
| cursor: pointer; | |
| transition: all 0.3s; | |
| } | |
| .upload-section:hover { | |
| background: #f0f1ff; | |
| border-color: #764ba2; | |
| } | |
| .upload-section.dragover { | |
| background: #e8e9ff; | |
| border-color: #764ba2; | |
| transform: scale(1.02); | |
| } | |
| input[type="file"] { | |
| display: none; | |
| } | |
| .upload-btn { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| border: none; | |
| padding: 15px 40px; | |
| font-size: 1.1em; | |
| border-radius: 8px; | |
| cursor: pointer; | |
| transition: transform 0.2s; | |
| font-weight: 600; | |
| } | |
| .upload-btn:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4); | |
| } | |
| .workspace { | |
| display: none; | |
| margin-top: 30px; | |
| } | |
| .workspace.active { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 30px; | |
| } | |
| .canvas-container { | |
| position: relative; | |
| background: #f8f9ff; | |
| border-radius: 12px; | |
| padding: 20px; | |
| box-shadow: inset 0 2px 8px rgba(0,0,0,0.1); | |
| } | |
| .canvas-title { | |
| font-size: 1.2em; | |
| font-weight: 600; | |
| margin-bottom: 15px; | |
| color: #333; | |
| } | |
| canvas { | |
| display: block; | |
| max-width: 100%; | |
| border-radius: 8px; | |
| box-shadow: 0 4px 12px rgba(0,0,0,0.15); | |
| cursor: crosshair; | |
| } | |
| .instructions { | |
| background: #fff3cd; | |
| border: 2px solid #ffc107; | |
| border-radius: 8px; | |
| padding: 15px; | |
| margin-bottom: 20px; | |
| font-size: 0.95em; | |
| line-height: 1.6; | |
| } | |
| .instructions strong { | |
| color: #856404; | |
| } | |
| .controls { | |
| display: flex; | |
| gap: 15px; | |
| margin-top: 20px; | |
| flex-wrap: wrap; | |
| } | |
| button { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| border: none; | |
| padding: 12px 30px; | |
| font-size: 1em; | |
| border-radius: 8px; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| font-weight: 600; | |
| } | |
| button:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4); | |
| } | |
| button:disabled { | |
| background: #ccc; | |
| cursor: not-allowed; | |
| transform: none; | |
| } | |
| button.clear { | |
| background: #dc3545; | |
| } | |
| button.clear:hover { | |
| box-shadow: 0 5px 15px rgba(220, 53, 69, 0.4); | |
| } | |
| .loading { | |
| display: none; | |
| text-align: center; | |
| padding: 30px; | |
| } | |
| .loading.active { | |
| display: block; | |
| } | |
| .spinner { | |
| border: 4px solid #f3f3f3; | |
| border-top: 4px solid #667eea; | |
| border-radius: 50%; | |
| width: 50px; | |
| height: 50px; | |
| animation: spin 1s linear infinite; | |
| margin: 0 auto 15px; | |
| } | |
| @keyframes spin { | |
| 0% { transform: rotate(0deg); } | |
| 100% { transform: rotate(360deg); } | |
| } | |
| .result-info { | |
| background: #d1ecf1; | |
| border: 2px solid #bee5eb; | |
| border-radius: 8px; | |
| padding: 15px; | |
| margin-top: 15px; | |
| font-size: 0.9em; | |
| } | |
| .result-info h4 { | |
| margin-bottom: 10px; | |
| color: #0c5460; | |
| } | |
| .result-info p { | |
| margin: 5px 0; | |
| color: #0c5460; | |
| } | |
| .error { | |
| background: #f8d7da; | |
| border: 2px solid #f5c6cb; | |
| color: #721c24; | |
| padding: 15px; | |
| border-radius: 8px; | |
| margin-top: 15px; | |
| display: none; | |
| } | |
| .error.active { | |
| display: block; | |
| } | |
| @media (max-width: 1200px) { | |
| .workspace.active { | |
| grid-template-columns: 1fr; | |
| } | |
| } | |
| .badge { | |
| display: inline-block; | |
| background: #28a745; | |
| color: white; | |
| padding: 4px 12px; | |
| border-radius: 12px; | |
| font-size: 0.85em; | |
| font-weight: 600; | |
| margin-left: 10px; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <h1>🎯 SAM2 Box Prompt Demo</h1> | |
| <p class="subtitle">Выдели объект прямоугольником → Получи точную сегментацию</p> | |
| <div class="panel"> | |
| <div class="upload-section" id="uploadSection"> | |
| <h2 style="margin-bottom: 15px;">📸 Загрузи изображение</h2> | |
| <p style="color: #666; margin-bottom: 20px;">Кликни или перетащи фото сюда</p> | |
| <button class="upload-btn" onclick="document.getElementById('fileInput').click()"> | |
| Выбрать файл | |
| </button> | |
| <input type="file" id="fileInput" accept="image/*"> | |
| </div> | |
| <div class="workspace" id="workspace"> | |
| <div class="instructions"> | |
| <strong>📝 Как пользоваться:</strong><br> | |
| 1. Зажми левую кнопку мыши и нарисуй прямоугольник вокруг объекта<br> | |
| 2. Нажми "Сегментировать" чтобы получить результат<br> | |
| 3. Справа увидишь вырезанный объект с прозрачностью | |
| </div> | |
| <div> | |
| <div class="canvas-container"> | |
| <div class="canvas-title"> | |
| Исходное изображение | |
| <span class="badge" id="imageSizeBadge"></span> | |
| </div> | |
| <canvas id="sourceCanvas"></canvas> | |
| </div> | |
| <div class="controls"> | |
| <button id="segmentBtn" onclick="segmentImage()"> | |
| 🚀 Сегментировать | |
| </button> | |
| <button class="clear" onclick="clearBox()"> | |
| 🗑️ Очистить выделение | |
| </button> | |
| <button class="clear" onclick="resetAll()"> | |
| 🔄 Новое изображение | |
| </button> | |
| </div> | |
| </div> | |
| <div> | |
| <div class="canvas-container"> | |
| <div class="canvas-title">Результат (вырезанный объект)</div> | |
| <canvas id="resultCanvas"></canvas> | |
| </div> | |
| <div class="loading" id="loading"> | |
| <div class="spinner"></div> | |
| <p>Обрабатываю изображение...</p> | |
| </div> | |
| <div class="result-info" id="resultInfo" style="display: none;"> | |
| <h4>📊 Информация о сегменте:</h4> | |
| <p><strong>Площадь:</strong> <span id="resultArea"></span> пикселей</p> | |
| <p><strong>BBox:</strong> <span id="resultBBox"></span></p> | |
| <p><strong>Confidence:</strong> <span id="resultConfidence"></span></p> | |
| </div> | |
| <div class="error" id="error"></div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| const API_URL = 'http://localhost:8000'; | |
| let sourceCanvas = document.getElementById('sourceCanvas'); | |
| let resultCanvas = document.getElementById('resultCanvas'); | |
| let sourceCtx = sourceCanvas.getContext('2d'); | |
| let resultCtx = resultCanvas.getContext('2d'); | |
| let currentImage = null; | |
| let isDrawing = false; | |
| let startX, startY, endX, endY; | |
| let boxCoords = null; | |
| // Drag & Drop | |
| const uploadSection = document.getElementById('uploadSection'); | |
| uploadSection.addEventListener('dragover', (e) => { | |
| e.preventDefault(); | |
| uploadSection.classList.add('dragover'); | |
| }); | |
| uploadSection.addEventListener('dragleave', () => { | |
| uploadSection.classList.remove('dragover'); | |
| }); | |
| uploadSection.addEventListener('drop', (e) => { | |
| e.preventDefault(); | |
| uploadSection.classList.remove('dragover'); | |
| const file = e.dataTransfer.files[0]; | |
| if (file && file.type.startsWith('image/')) { | |
| loadImage(file); | |
| } | |
| }); | |
| // File input | |
| document.getElementById('fileInput').addEventListener('change', (e) => { | |
| const file = e.target.files[0]; | |
| if (file) { | |
| loadImage(file); | |
| } | |
| }); | |
| function loadImage(file) { | |
| const reader = new FileReader(); | |
| reader.onload = (e) => { | |
| const img = new Image(); | |
| img.onload = () => { | |
| currentImage = img; | |
| setupCanvas(img); | |
| document.getElementById('workspace').classList.add('active'); | |
| document.getElementById('imageSizeBadge').textContent = `${img.width}×${img.height}`; | |
| }; | |
| img.src = e.target.result; | |
| }; | |
| reader.readAsDataURL(file); | |
| } | |
| function setupCanvas(img) { | |
| const maxWidth = 600; | |
| const scale = Math.min(1, maxWidth / img.width); | |
| sourceCanvas.width = img.width * scale; | |
| sourceCanvas.height = img.height * scale; | |
| resultCanvas.width = sourceCanvas.width; | |
| resultCanvas.height = sourceCanvas.height; | |
| sourceCtx.drawImage(img, 0, 0, sourceCanvas.width, sourceCanvas.height); | |
| resultCtx.clearRect(0, 0, resultCanvas.width, resultCanvas.height); | |
| boxCoords = null; | |
| document.getElementById('resultInfo').style.display = 'none'; | |
| document.getElementById('error').classList.remove('active'); | |
| } | |
| // Рисование бокса | |
| sourceCanvas.addEventListener('mousedown', (e) => { | |
| const rect = sourceCanvas.getBoundingClientRect(); | |
| startX = e.clientX - rect.left; | |
| startY = e.clientY - rect.top; | |
| isDrawing = true; | |
| }); | |
| sourceCanvas.addEventListener('mousemove', (e) => { | |
| if (!isDrawing) return; | |
| const rect = sourceCanvas.getBoundingClientRect(); | |
| endX = e.clientX - rect.left; | |
| endY = e.clientY - rect.top; | |
| redrawCanvas(); | |
| drawBox(startX, startY, endX, endY); | |
| }); | |
| sourceCanvas.addEventListener('mouseup', (e) => { | |
| if (!isDrawing) return; | |
| const rect = sourceCanvas.getBoundingClientRect(); | |
| endX = e.clientX - rect.left; | |
| endY = e.clientY - rect.top; | |
| isDrawing = false; | |
| // Сохраняем координаты в масштабе оригинального изображения | |
| const scaleX = currentImage.width / sourceCanvas.width; | |
| const scaleY = currentImage.height / sourceCanvas.height; | |
| boxCoords = { | |
| x1: Math.min(startX, endX) * scaleX, | |
| y1: Math.min(startY, endY) * scaleY, | |
| x2: Math.max(startX, endX) * scaleX, | |
| y2: Math.max(startY, endY) * scaleY | |
| }; | |
| console.log('Box coordinates:', boxCoords); | |
| }); | |
| function redrawCanvas() { | |
| sourceCtx.clearRect(0, 0, sourceCanvas.width, sourceCanvas.height); | |
| sourceCtx.drawImage(currentImage, 0, 0, sourceCanvas.width, sourceCanvas.height); | |
| } | |
| function drawBox(x1, y1, x2, y2) { | |
| const width = x2 - x1; | |
| const height = y2 - y1; | |
| sourceCtx.strokeStyle = '#00ff00'; | |
| sourceCtx.lineWidth = 3; | |
| sourceCtx.setLineDash([10, 5]); | |
| sourceCtx.strokeRect(x1, y1, width, height); | |
| // Полупрозрачная заливка | |
| sourceCtx.fillStyle = 'rgba(0, 255, 0, 0.1)'; | |
| sourceCtx.fillRect(x1, y1, width, height); | |
| sourceCtx.setLineDash([]); | |
| } | |
| function clearBox() { | |
| boxCoords = null; | |
| redrawCanvas(); | |
| resultCtx.clearRect(0, 0, resultCanvas.width, resultCanvas.height); | |
| document.getElementById('resultInfo').style.display = 'none'; | |
| } | |
| function resetAll() { | |
| currentImage = null; | |
| boxCoords = null; | |
| document.getElementById('workspace').classList.remove('active'); | |
| document.getElementById('fileInput').value = ''; | |
| document.getElementById('resultInfo').style.display = 'none'; | |
| document.getElementById('error').classList.remove('active'); | |
| } | |
| async function segmentImage() { | |
| if (!boxCoords) { | |
| showError('Сначала нарисуй прямоугольник на изображении!'); | |
| return; | |
| } | |
| const { x1, y1, x2, y2 } = boxCoords; | |
| if (x2 - x1 < 10 || y2 - y1 < 10) { | |
| showError('Выделенная область слишком маленькая. Нарисуй больший прямоугольник.'); | |
| return; | |
| } | |
| document.getElementById('loading').classList.add('active'); | |
| document.getElementById('error').classList.remove('active'); | |
| document.getElementById('segmentBtn').disabled = true; | |
| try { | |
| // Конвертируем изображение в blob | |
| const canvas = document.createElement('canvas'); | |
| canvas.width = currentImage.width; | |
| canvas.height = currentImage.height; | |
| const ctx = canvas.getContext('2d'); | |
| ctx.drawImage(currentImage, 0, 0); | |
| const blob = await new Promise(resolve => canvas.toBlob(resolve, 'image/jpeg', 0.95)); | |
| // Формируем запрос | |
| const formData = new FormData(); | |
| formData.append('file', blob, 'image.jpg'); | |
| const url = `${API_URL}/segment?box_x1=${x1}&box_y1=${y1}&box_x2=${x2}&box_y2=${y2}&extract_objects=true&include_masks=false`; | |
| console.log('Request URL:', url); | |
| const response = await fetch(url, { | |
| method: 'POST', | |
| body: formData | |
| }); | |
| if (!response.ok) { | |
| throw new Error(`HTTP ${response.status}: ${await response.text()}`); | |
| } | |
| const result = await response.json(); | |
| console.log('Result:', result); | |
| if (result.success && result.segments.length > 0) { | |
| displayResult(result.segments[0]); | |
| } else { | |
| showError('Не удалось найти объект в выделенной области'); | |
| } | |
| } catch (error) { | |
| console.error('Error:', error); | |
| showError(`Ошибка: ${error.message}`); | |
| } finally { | |
| document.getElementById('loading').classList.remove('active'); | |
| document.getElementById('segmentBtn').disabled = false; | |
| } | |
| } | |
| function displayResult(segment) { | |
| if (!segment.extracted_image) { | |
| showError('Не получен вырезанный объект'); | |
| return; | |
| } | |
| const img = new Image(); | |
| img.onload = () => { | |
| resultCtx.clearRect(0, 0, resultCanvas.width, resultCanvas.height); | |
| // Рисуем в центре canvas | |
| const scale = Math.min( | |
| resultCanvas.width / img.width, | |
| resultCanvas.height / img.height, | |
| 1 | |
| ); | |
| const w = img.width * scale; | |
| const h = img.height * scale; | |
| const x = (resultCanvas.width - w) / 2; | |
| const y = (resultCanvas.height - h) / 2; | |
| // Клетчатый фон для прозрачности | |
| drawCheckerboard(resultCtx, x, y, w, h); | |
| resultCtx.drawImage(img, x, y, w, h); | |
| // Показываем инфу | |
| document.getElementById('resultArea').textContent = segment.area.toLocaleString(); | |
| document.getElementById('resultBBox').textContent = | |
| `${segment.bbox.width}×${segment.bbox.height} px`; | |
| document.getElementById('resultConfidence').textContent = | |
| `${(segment.confidence * 100).toFixed(1)}%`; | |
| document.getElementById('resultInfo').style.display = 'block'; | |
| }; | |
| img.src = segment.extracted_image; | |
| } | |
| function drawCheckerboard(ctx, x, y, w, h) { | |
| const size = 10; | |
| ctx.fillStyle = '#f0f0f0'; | |
| ctx.fillRect(x, y, w, h); | |
| ctx.fillStyle = '#ddd'; | |
| for (let i = 0; i < w; i += size) { | |
| for (let j = 0; j < h; j += size) { | |
| if ((i / size + j / size) % 2 === 0) { | |
| ctx.fillRect(x + i, y + j, size, size); | |
| } | |
| } | |
| } | |
| } | |
| function showError(message) { | |
| const errorEl = document.getElementById('error'); | |
| errorEl.textContent = '❌ ' + message; | |
| errorEl.classList.add('active'); | |
| } | |
| </script> | |
| </body> | |
| </html> | |