Spaces:
Running
Running
| <html lang="ru"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>SAM2 Advanced Demo - Box + Brush</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: 1600px; | |
| 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); | |
| } | |
| .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; | |
| } | |
| 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-wrapper { | |
| position: relative; | |
| display: inline-block; | |
| } | |
| canvas { | |
| display: block; | |
| max-width: 100%; | |
| border-radius: 8px; | |
| box-shadow: 0 4px 12px rgba(0,0,0,0.15); | |
| } | |
| #sourceCanvas { | |
| cursor: crosshair; | |
| } | |
| #drawCanvas { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| pointer-events: none; | |
| } | |
| .tools { | |
| display: flex; | |
| gap: 10px; | |
| margin-bottom: 15px; | |
| flex-wrap: wrap; | |
| align-items: center; | |
| } | |
| .tool-btn { | |
| padding: 10px 20px; | |
| border: 2px solid #ddd; | |
| background: white; | |
| border-radius: 8px; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| font-weight: 600; | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .tool-btn:hover { | |
| border-color: #667eea; | |
| background: #f8f9ff; | |
| } | |
| .tool-btn.active { | |
| border-color: #667eea; | |
| background: #667eea; | |
| color: white; | |
| } | |
| .tool-btn.box { border-color: #007bff; } | |
| .tool-btn.box.active { background: #007bff; } | |
| .tool-btn.brush-fg { border-color: #28a745; } | |
| .tool-btn.brush-fg.active { background: #28a745; } | |
| .tool-btn.brush-bg { border-color: #dc3545; } | |
| .tool-btn.brush-bg.active { background: #dc3545; } | |
| .brush-size { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| padding: 8px 15px; | |
| background: #f8f9ff; | |
| border-radius: 8px; | |
| } | |
| .brush-size label { | |
| font-size: 0.9em; | |
| font-weight: 600; | |
| } | |
| .brush-size input { | |
| width: 100px; | |
| } | |
| .instructions { | |
| background: #fff3cd; | |
| border: 2px solid #ffc107; | |
| border-radius: 8px; | |
| padding: 15px; | |
| margin-bottom: 20px; | |
| font-size: 0.9em; | |
| line-height: 1.6; | |
| } | |
| .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; | |
| } | |
| .badge { | |
| display: inline-block; | |
| background: #28a745; | |
| color: white; | |
| padding: 4px 12px; | |
| border-radius: 12px; | |
| font-size: 0.85em; | |
| font-weight: 600; | |
| margin-left: 10px; | |
| } | |
| .color-indicator { | |
| width: 20px; | |
| height: 20px; | |
| border-radius: 50%; | |
| display: inline-block; | |
| border: 2px solid #666; | |
| } | |
| @media (max-width: 1400px) { | |
| .workspace.active { | |
| grid-template-columns: 1fr; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <h1>🎨 SAM2 Advanced Demo</h1> | |
| <p class="subtitle">Box выделение ИЛИ рисование кистью (зеленый = объект, красный = фон)</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> | |
| 🟦 <strong>Box</strong> - рисуй прямоугольник вокруг объекта<br> | |
| 🟢 <strong>Brush (FG)</strong> - закрашивай объект зеленым<br> | |
| 🔴 <strong>Brush (BG)</strong> - закрашивай фон красным для исключения<br><br> | |
| 💡 <strong>Совет:</strong> Можно комбинировать! Например, нарисуй бокс + закрась проблемные зоны | |
| </div> | |
| <div> | |
| <div class="canvas-container"> | |
| <div class="canvas-title"> | |
| Исходное изображение | |
| <span class="badge" id="imageSizeBadge"></span> | |
| </div> | |
| <div class="tools"> | |
| <div class="tool-btn box active" id="toolBox" onclick="setTool('box')"> | |
| 📦 Box | |
| </div> | |
| <div class="tool-btn brush-fg" id="toolBrushFG" onclick="setTool('brush-fg')"> | |
| 🟢 Brush (Объект) | |
| </div> | |
| <div class="tool-btn brush-bg" id="toolBrushBG" onclick="setTool('brush-bg')"> | |
| 🔴 Brush (Фон) | |
| </div> | |
| <div class="brush-size"> | |
| <label>🖌️ Размер:</label> | |
| <input type="range" id="brushSize" min="5" max="50" value="15" | |
| oninput="updateBrushSize(this.value)"> | |
| <span id="brushSizeLabel">15px</span> | |
| </div> | |
| </div> | |
| <div class="canvas-wrapper"> | |
| <canvas id="sourceCanvas"></canvas> | |
| <canvas id="drawCanvas"></canvas> | |
| </div> | |
| </div> | |
| <div class="controls"> | |
| <button id="segmentBtn" onclick="segmentImage()"> | |
| 🚀 Сегментировать | |
| </button> | |
| <button class="clear" onclick="clearDrawing()"> | |
| 🗑️ Очистить рисунок | |
| </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 drawCanvas = document.getElementById('drawCanvas'); | |
| let resultCanvas = document.getElementById('resultCanvas'); | |
| let sourceCtx = sourceCanvas.getContext('2d'); | |
| let drawCtx = drawCanvas.getContext('2d'); | |
| let resultCtx = resultCanvas.getContext('2d'); | |
| let currentImage = null; | |
| let currentTool = 'box'; | |
| let brushSize = 15; | |
| 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); | |
| } | |
| }); | |
| 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 = 700; | |
| const scale = Math.min(1, maxWidth / img.width); | |
| sourceCanvas.width = img.width * scale; | |
| sourceCanvas.height = img.height * scale; | |
| drawCanvas.width = sourceCanvas.width; | |
| drawCanvas.height = sourceCanvas.height; | |
| resultCanvas.width = sourceCanvas.width; | |
| resultCanvas.height = sourceCanvas.height; | |
| sourceCtx.drawImage(img, 0, 0, sourceCanvas.width, sourceCanvas.height); | |
| drawCtx.clearRect(0, 0, drawCanvas.width, drawCanvas.height); | |
| resultCtx.clearRect(0, 0, resultCanvas.width, resultCanvas.height); | |
| boxCoords = null; | |
| document.getElementById('resultInfo').style.display = 'none'; | |
| document.getElementById('error').classList.remove('active'); | |
| } | |
| function setTool(tool) { | |
| currentTool = tool; | |
| document.querySelectorAll('.tool-btn').forEach(btn => btn.classList.remove('active')); | |
| document.getElementById('tool' + tool.split('-').map(w => w[0].toUpperCase() + w.slice(1)).join('')).classList.add('active'); | |
| if (tool === 'box') { | |
| sourceCanvas.style.cursor = 'crosshair'; | |
| } else { | |
| sourceCanvas.style.cursor = `url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="${brushSize*2}" height="${brushSize*2}"><circle cx="${brushSize}" cy="${brushSize}" r="${brushSize}" fill="none" stroke="black" stroke-width="2"/></svg>') ${brushSize} ${brushSize}, crosshair`; | |
| } | |
| } | |
| function updateBrushSize(size) { | |
| brushSize = parseInt(size); | |
| document.getElementById('brushSizeLabel').textContent = brushSize + 'px'; | |
| if (currentTool !== 'box') { | |
| setTool(currentTool); | |
| } | |
| } | |
| // Mouse events | |
| sourceCanvas.addEventListener('mousedown', (e) => { | |
| const rect = sourceCanvas.getBoundingClientRect(); | |
| startX = e.clientX - rect.left; | |
| startY = e.clientY - rect.top; | |
| isDrawing = true; | |
| if (currentTool !== 'box') { | |
| drawBrush(startX, startY); | |
| } | |
| }); | |
| sourceCanvas.addEventListener('mousemove', (e) => { | |
| if (!isDrawing) return; | |
| const rect = sourceCanvas.getBoundingClientRect(); | |
| endX = e.clientX - rect.left; | |
| endY = e.clientY - rect.top; | |
| if (currentTool === 'box') { | |
| redrawCanvas(); | |
| drawBox(startX, startY, endX, endY); | |
| } else { | |
| drawBrush(endX, endY); | |
| startX = endX; | |
| startY = endY; | |
| } | |
| }); | |
| sourceCanvas.addEventListener('mouseup', (e) => { | |
| if (!isDrawing) return; | |
| isDrawing = false; | |
| if (currentTool === 'box') { | |
| const rect = sourceCanvas.getBoundingClientRect(); | |
| endX = e.clientX - rect.left; | |
| endY = e.clientY - rect.top; | |
| 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 | |
| }; | |
| } | |
| }); | |
| function drawBrush(x, y) { | |
| drawCtx.globalAlpha = 0.6; | |
| drawCtx.lineCap = 'round'; | |
| drawCtx.lineJoin = 'round'; | |
| drawCtx.lineWidth = brushSize * 2; | |
| if (currentTool === 'brush-fg') { | |
| drawCtx.strokeStyle = '#00ff00'; | |
| drawCtx.fillStyle = '#00ff00'; | |
| } else { | |
| drawCtx.strokeStyle = '#ff0000'; | |
| drawCtx.fillStyle = '#ff0000'; | |
| } | |
| drawCtx.beginPath(); | |
| drawCtx.arc(x, y, brushSize, 0, Math.PI * 2); | |
| drawCtx.fill(); | |
| } | |
| function redrawCanvas() { | |
| drawCtx.clearRect(0, 0, drawCanvas.width, drawCanvas.height); | |
| } | |
| function drawBox(x1, y1, x2, y2) { | |
| const width = x2 - x1; | |
| const height = y2 - y1; | |
| drawCtx.strokeStyle = '#00ff00'; | |
| drawCtx.lineWidth = 3; | |
| drawCtx.setLineDash([10, 5]); | |
| drawCtx.strokeRect(x1, y1, width, height); | |
| drawCtx.fillStyle = 'rgba(0, 255, 0, 0.1)'; | |
| drawCtx.fillRect(x1, y1, width, height); | |
| drawCtx.setLineDash([]); | |
| } | |
| function clearDrawing() { | |
| boxCoords = null; | |
| drawCtx.clearRect(0, 0, drawCanvas.width, drawCanvas.height); | |
| 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() { | |
| // Проверяем есть ли промпты | |
| const hasDrawing = !isCanvasEmpty(drawCanvas); | |
| if (!boxCoords && !hasDrawing) { | |
| showError('Нарисуй прямоугольник или закрась области кистью!'); | |
| return; | |
| } | |
| document.getElementById('loading').classList.add('active'); | |
| document.getElementById('error').classList.remove('active'); | |
| document.getElementById('segmentBtn').disabled = true; | |
| try { | |
| // Конвертируем изображение | |
| 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'); | |
| // Строим URL с параметрами | |
| const params = new URLSearchParams({ | |
| extract_objects: true, | |
| include_masks: false | |
| }); | |
| // Добавляем box если есть | |
| if (boxCoords) { | |
| params.append('box_x1', boxCoords.x1); | |
| params.append('box_y1', boxCoords.y1); | |
| params.append('box_x2', boxCoords.x2); | |
| params.append('box_y2', boxCoords.y2); | |
| } | |
| // Добавляем маску если есть рисунок | |
| if (hasDrawing) { | |
| const maskData = drawCanvas.toDataURL('image/png'); | |
| params.append('mask_data', maskData); | |
| } | |
| const url = `${API_URL}/segment?${params}`; | |
| 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 isCanvasEmpty(canvas) { | |
| const ctx = canvas.getContext('2d'); | |
| const pixelData = ctx.getImageData(0, 0, canvas.width, canvas.height).data; | |
| for (let i = 3; i < pixelData.length; i += 4) { | |
| if (pixelData[i] !== 0) return false; | |
| } | |
| return true; | |
| } | |
| function displayResult(segment) { | |
| if (!segment.extracted_image) { | |
| showError('Не получен вырезанный объект'); | |
| return; | |
| } | |
| const img = new Image(); | |
| img.onload = () => { | |
| resultCtx.clearRect(0, 0, resultCanvas.width, resultCanvas.height); | |
| 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'); | |
| } | |
| // Инициализация | |
| setTool('box'); | |
| </script> | |
| </body> | |
| </html> | |