gbreadman13/sam2-api
0
1<!DOCTYPE html>2<html lang="ru">3<head>4 <meta charset="UTF-8">5 <meta name="viewport" content="width=device-width, initial-scale=1.0">6 <title>SAM2 Advanced Demo - Box + Brush</title>7 <style>8 * {9 margin: 0;10 padding: 0;11 box-sizing: border-box;12 }13 14 body {15 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;16 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);17 min-height: 100vh;18 padding: 20px;19 }20 21 .container {22 max-width: 1600px;23 margin: 0 auto;24 }25 26 h1 {27 color: white;28 text-align: center;29 margin-bottom: 10px;30 font-size: 2.5em;31 text-shadow: 2px 2px 4px rgba(0,0,0,0.3);32 }33 34 .subtitle {35 color: rgba(255,255,255,0.9);36 text-align: center;37 margin-bottom: 30px;38 font-size: 1.1em;39 }40 41 .panel {42 background: white;43 border-radius: 16px;44 padding: 30px;45 box-shadow: 0 20px 60px rgba(0,0,0,0.3);46 }47 48 .upload-section {49 text-align: center;50 padding: 40px;51 border: 3px dashed #667eea;52 border-radius: 12px;53 background: #f8f9ff;54 cursor: pointer;55 transition: all 0.3s;56 }57 58 .upload-section:hover {59 background: #f0f1ff;60 border-color: #764ba2;61 }62 63 input[type="file"] {64 display: none;65 }66 67 .upload-btn {68 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);69 color: white;70 border: none;71 padding: 15px 40px;72 font-size: 1.1em;73 border-radius: 8px;74 cursor: pointer;75 transition: transform 0.2s;76 font-weight: 600;77 }78 79 .upload-btn:hover {80 transform: translateY(-2px);81 box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);82 }83 84 .workspace {85 display: none;86 margin-top: 30px;87 }88 89 .workspace.active {90 display: grid;91 grid-template-columns: 1fr 1fr;92 gap: 30px;93 }94 95 .canvas-container {96 position: relative;97 background: #f8f9ff;98 border-radius: 12px;99 padding: 20px;100 box-shadow: inset 0 2px 8px rgba(0,0,0,0.1);101 }102 103 .canvas-title {104 font-size: 1.2em;105 font-weight: 600;106 margin-bottom: 15px;107 color: #333;108 }109 110 .canvas-wrapper {111 position: relative;112 display: inline-block;113 }114 115 canvas {116 display: block;117 max-width: 100%;118 border-radius: 8px;119 box-shadow: 0 4px 12px rgba(0,0,0,0.15);120 }121 122 #sourceCanvas {123 cursor: crosshair;124 }125 126 #drawCanvas {127 position: absolute;128 top: 0;129 left: 0;130 pointer-events: none;131 }132 133 .tools {134 display: flex;135 gap: 10px;136 margin-bottom: 15px;137 flex-wrap: wrap;138 align-items: center;139 }140 141 .tool-btn {142 padding: 10px 20px;143 border: 2px solid #ddd;144 background: white;145 border-radius: 8px;146 cursor: pointer;147 transition: all 0.2s;148 font-weight: 600;149 display: flex;150 align-items: center;151 gap: 8px;152 }153 154 .tool-btn:hover {155 border-color: #667eea;156 background: #f8f9ff;157 }158 159 .tool-btn.active {160 border-color: #667eea;161 background: #667eea;162 color: white;163 }164 165 .tool-btn.box { border-color: #007bff; }166 .tool-btn.box.active { background: #007bff; }167 168 .tool-btn.brush-fg { border-color: #28a745; }169 .tool-btn.brush-fg.active { background: #28a745; }170 171 .tool-btn.brush-bg { border-color: #dc3545; }172 .tool-btn.brush-bg.active { background: #dc3545; }173 174 .brush-size {175 display: flex;176 align-items: center;177 gap: 10px;178 padding: 8px 15px;179 background: #f8f9ff;180 border-radius: 8px;181 }182 183 .brush-size label {184 font-size: 0.9em;185 font-weight: 600;186 }187 188 .brush-size input {189 width: 100px;190 }191 192 .instructions {193 background: #fff3cd;194 border: 2px solid #ffc107;195 border-radius: 8px;196 padding: 15px;197 margin-bottom: 20px;198 font-size: 0.9em;199 line-height: 1.6;200 }201 202 .controls {203 display: flex;204 gap: 15px;205 margin-top: 20px;206 flex-wrap: wrap;207 }208 209 button {210 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);211 color: white;212 border: none;213 padding: 12px 30px;214 font-size: 1em;215 border-radius: 8px;216 cursor: pointer;217 transition: all 0.2s;218 font-weight: 600;219 }220 221 button:hover {222 transform: translateY(-2px);223 box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);224 }225 226 button:disabled {227 background: #ccc;228 cursor: not-allowed;229 transform: none;230 }231 232 button.clear {233 background: #dc3545;234 }235 236 button.clear:hover {237 box-shadow: 0 5px 15px rgba(220, 53, 69, 0.4);238 }239 240 .loading {241 display: none;242 text-align: center;243 padding: 30px;244 }245 246 .loading.active {247 display: block;248 }249 250 .spinner {251 border: 4px solid #f3f3f3;252 border-top: 4px solid #667eea;253 border-radius: 50%;254 width: 50px;255 height: 50px;256 animation: spin 1s linear infinite;257 margin: 0 auto 15px;258 }259 260 @keyframes spin {261 0% { transform: rotate(0deg); }262 100% { transform: rotate(360deg); }263 }264 265 .result-info {266 background: #d1ecf1;267 border: 2px solid #bee5eb;268 border-radius: 8px;269 padding: 15px;270 margin-top: 15px;271 font-size: 0.9em;272 }273 274 .result-info h4 {275 margin-bottom: 10px;276 color: #0c5460;277 }278 279 .result-info p {280 margin: 5px 0;281 color: #0c5460;282 }283 284 .error {285 background: #f8d7da;286 border: 2px solid #f5c6cb;287 color: #721c24;288 padding: 15px;289 border-radius: 8px;290 margin-top: 15px;291 display: none;292 }293 294 .error.active {295 display: block;296 }297 298 .badge {299 display: inline-block;300 background: #28a745;301 color: white;302 padding: 4px 12px;303 border-radius: 12px;304 font-size: 0.85em;305 font-weight: 600;306 margin-left: 10px;307 }308 309 .color-indicator {310 width: 20px;311 height: 20px;312 border-radius: 50%;313 display: inline-block;314 border: 2px solid #666;315 }316 317 @media (max-width: 1400px) {318 .workspace.active {319 grid-template-columns: 1fr;320 }321 }322 </style>323</head>324<body>325 <div class="container">326 <h1>🎨 SAM2 Advanced Demo</h1>327 <p class="subtitle">Box выделение ИЛИ рисование кистью (зеленый = объект, красный = фон)</p>328 329 <div class="panel">330 <div class="upload-section" id="uploadSection">331 <h2 style="margin-bottom: 15px;">📸 Загрузи изображение</h2>332 <p style="color: #666; margin-bottom: 20px;">Кликни или перетащи фото сюда</p>333 <button class="upload-btn" onclick="document.getElementById('fileInput').click()">334 Выбрать файл335 </button>336 <input type="file" id="fileInput" accept="image/*">337 </div>338 339 <div class="workspace" id="workspace">340 <div class="instructions">341 <strong>📝 Режимы работы:</strong><br>342 🟦 <strong>Box</strong> - рисуй прямоугольник вокруг объекта<br>343 🟢 <strong>Brush (FG)</strong> - закрашивай объект зеленым<br>344 🔴 <strong>Brush (BG)</strong> - закрашивай фон красным для исключения<br><br>345 💡 <strong>Совет:</strong> Можно комбинировать! Например, нарисуй бокс + закрась проблемные зоны346 </div>347 348 <div>349 <div class="canvas-container">350 <div class="canvas-title">351 Исходное изображение 352 <span class="badge" id="imageSizeBadge"></span>353 </div>354 355 <div class="tools">356 <div class="tool-btn box active" id="toolBox" onclick="setTool('box')">357 📦 Box358 </div>359 <div class="tool-btn brush-fg" id="toolBrushFG" onclick="setTool('brush-fg')">360 🟢 Brush (Объект)361 </div>362 <div class="tool-btn brush-bg" id="toolBrushBG" onclick="setTool('brush-bg')">363 🔴 Brush (Фон)364 </div>365 <div class="brush-size">366 <label>🖌️ Размер:</label>367 <input type="range" id="brushSize" min="5" max="50" value="15" 368 oninput="updateBrushSize(this.value)">369 <span id="brushSizeLabel">15px</span>370 </div>371 </div>372 373 <div class="canvas-wrapper">374 <canvas id="sourceCanvas"></canvas>375 <canvas id="drawCanvas"></canvas>376 </div>377 </div>378 379 <div class="controls">380 <button id="segmentBtn" onclick="segmentImage()">381 🚀 Сегментировать382 </button>383 <button class="clear" onclick="clearDrawing()">384 🗑️ Очистить рисунок385 </button>386 <button class="clear" onclick="resetAll()">387 🔄 Новое изображение388 </button>389 </div>390 </div>391 392 <div>393 <div class="canvas-container">394 <div class="canvas-title">Результат (вырезанный объект)</div>395 <canvas id="resultCanvas"></canvas>396 </div>397 398 <div class="loading" id="loading">399 <div class="spinner"></div>400 <p>Обрабатываю изображение...</p>401 </div>402 403 <div class="result-info" id="resultInfo" style="display: none;">404 <h4>📊 Информация о сегменте:</h4>405 <p><strong>Площадь:</strong> <span id="resultArea"></span> пикселей</p>406 <p><strong>BBox:</strong> <span id="resultBBox"></span></p>407 <p><strong>Confidence:</strong> <span id="resultConfidence"></span></p>408 </div>409 410 <div class="error" id="error"></div>411 </div>412 </div>413 </div>414 </div>415 416 <script>417 const API_URL = 'http://localhost:8000';418 419 let sourceCanvas = document.getElementById('sourceCanvas');420 let drawCanvas = document.getElementById('drawCanvas');421 let resultCanvas = document.getElementById('resultCanvas');422 let sourceCtx = sourceCanvas.getContext('2d');423 let drawCtx = drawCanvas.getContext('2d');424 let resultCtx = resultCanvas.getContext('2d');425 426 let currentImage = null;427 let currentTool = 'box';428 let brushSize = 15;429 let isDrawing = false;430 let startX, startY, endX, endY;431 let boxCoords = null;432 433 // Drag & Drop434 const uploadSection = document.getElementById('uploadSection');435 436 uploadSection.addEventListener('dragover', (e) => {437 e.preventDefault();438 uploadSection.classList.add('dragover');439 });440 441 uploadSection.addEventListener('dragleave', () => {442 uploadSection.classList.remove('dragover');443 });444 445 uploadSection.addEventListener('drop', (e) => {446 e.preventDefault();447 uploadSection.classList.remove('dragover');448 const file = e.dataTransfer.files[0];449 if (file && file.type.startsWith('image/')) {450 loadImage(file);451 }452 });453 454 document.getElementById('fileInput').addEventListener('change', (e) => {455 const file = e.target.files[0];456 if (file) loadImage(file);457 });458 459 function loadImage(file) {460 const reader = new FileReader();461 reader.onload = (e) => {462 const img = new Image();463 img.onload = () => {464 currentImage = img;465 setupCanvas(img);466 document.getElementById('workspace').classList.add('active');467 document.getElementById('imageSizeBadge').textContent = `${img.width}×${img.height}`;468 };469 img.src = e.target.result;470 };471 reader.readAsDataURL(file);472 }473 474 function setupCanvas(img) {475 const maxWidth = 700;476 const scale = Math.min(1, maxWidth / img.width);477 478 sourceCanvas.width = img.width * scale;479 sourceCanvas.height = img.height * scale;480 drawCanvas.width = sourceCanvas.width;481 drawCanvas.height = sourceCanvas.height;482 resultCanvas.width = sourceCanvas.width;483 resultCanvas.height = sourceCanvas.height;484 485 sourceCtx.drawImage(img, 0, 0, sourceCanvas.width, sourceCanvas.height);486 drawCtx.clearRect(0, 0, drawCanvas.width, drawCanvas.height);487 resultCtx.clearRect(0, 0, resultCanvas.width, resultCanvas.height);488 489 boxCoords = null;490 document.getElementById('resultInfo').style.display = 'none';491 document.getElementById('error').classList.remove('active');492 }493 494 function setTool(tool) {495 currentTool = tool;496 document.querySelectorAll('.tool-btn').forEach(btn => btn.classList.remove('active'));497 document.getElementById('tool' + tool.split('-').map(w => w[0].toUpperCase() + w.slice(1)).join('')).classList.add('active');498 499 if (tool === 'box') {500 sourceCanvas.style.cursor = 'crosshair';501 } else {502 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`;503 }504 }505 506 function updateBrushSize(size) {507 brushSize = parseInt(size);508 document.getElementById('brushSizeLabel').textContent = brushSize + 'px';509 if (currentTool !== 'box') {510 setTool(currentTool);511 }512 }513 514 // Mouse events515 sourceCanvas.addEventListener('mousedown', (e) => {516 const rect = sourceCanvas.getBoundingClientRect();517 startX = e.clientX - rect.left;518 startY = e.clientY - rect.top;519 isDrawing = true;520 521 if (currentTool !== 'box') {522 drawBrush(startX, startY);523 }524 });525 526 sourceCanvas.addEventListener('mousemove', (e) => {527 if (!isDrawing) return;528 529 const rect = sourceCanvas.getBoundingClientRect();530 endX = e.clientX - rect.left;531 endY = e.clientY - rect.top;532 533 if (currentTool === 'box') {534 redrawCanvas();535 drawBox(startX, startY, endX, endY);536 } else {537 drawBrush(endX, endY);538 startX = endX;539 startY = endY;540 }541 });542 543 sourceCanvas.addEventListener('mouseup', (e) => {544 if (!isDrawing) return;545 isDrawing = false;546 547 if (currentTool === 'box') {548 const rect = sourceCanvas.getBoundingClientRect();549 endX = e.clientX - rect.left;550 endY = e.clientY - rect.top;551 552 const scaleX = currentImage.width / sourceCanvas.width;553 const scaleY = currentImage.height / sourceCanvas.height;554 555 boxCoords = {556 x1: Math.min(startX, endX) * scaleX,557 y1: Math.min(startY, endY) * scaleY,558 x2: Math.max(startX, endX) * scaleX,559 y2: Math.max(startY, endY) * scaleY560 };561 }562 });563 564 function drawBrush(x, y) {565 drawCtx.globalAlpha = 0.6;566 drawCtx.lineCap = 'round';567 drawCtx.lineJoin = 'round';568 drawCtx.lineWidth = brushSize * 2;569 570 if (currentTool === 'brush-fg') {571 drawCtx.strokeStyle = '#00ff00';572 drawCtx.fillStyle = '#00ff00';573 } else {574 drawCtx.strokeStyle = '#ff0000';575 drawCtx.fillStyle = '#ff0000';576 }577 578 drawCtx.beginPath();579 drawCtx.arc(x, y, brushSize, 0, Math.PI * 2);580 drawCtx.fill();581 }582 583 function redrawCanvas() {584 drawCtx.clearRect(0, 0, drawCanvas.width, drawCanvas.height);585 }586 587 function drawBox(x1, y1, x2, y2) {588 const width = x2 - x1;589 const height = y2 - y1;590 591 drawCtx.strokeStyle = '#00ff00';592 drawCtx.lineWidth = 3;593 drawCtx.setLineDash([10, 5]);594 drawCtx.strokeRect(x1, y1, width, height);595 596 drawCtx.fillStyle = 'rgba(0, 255, 0, 0.1)';597 drawCtx.fillRect(x1, y1, width, height);598 599 drawCtx.setLineDash([]);600 }601 602 function clearDrawing() {603 boxCoords = null;604 drawCtx.clearRect(0, 0, drawCanvas.width, drawCanvas.height);605 resultCtx.clearRect(0, 0, resultCanvas.width, resultCanvas.height);606 document.getElementById('resultInfo').style.display = 'none';607 }608 609 function resetAll() {610 currentImage = null;611 boxCoords = null;612 document.getElementById('workspace').classList.remove('active');613 document.getElementById('fileInput').value = '';614 document.getElementById('resultInfo').style.display = 'none';615 document.getElementById('error').classList.remove('active');616 }617 618 async function segmentImage() {619 // Проверяем есть ли промпты620 const hasDrawing = !isCanvasEmpty(drawCanvas);621 622 if (!boxCoords && !hasDrawing) {623 showError('Нарисуй прямоугольник или закрась области кистью!');624 return;625 }626 627 document.getElementById('loading').classList.add('active');628 document.getElementById('error').classList.remove('active');629 document.getElementById('segmentBtn').disabled = true;630 631 try {632 // Конвертируем изображение633 const canvas = document.createElement('canvas');634 canvas.width = currentImage.width;635 canvas.height = currentImage.height;636 const ctx = canvas.getContext('2d');637 ctx.drawImage(currentImage, 0, 0);638 639 const blob = await new Promise(resolve => canvas.toBlob(resolve, 'image/jpeg', 0.95));640 641 const formData = new FormData();642 formData.append('file', blob, 'image.jpg');643 644 // Строим URL с параметрами645 const params = new URLSearchParams({646 extract_objects: true,647 include_masks: false648 });649 650 // Добавляем box если есть651 if (boxCoords) {652 params.append('box_x1', boxCoords.x1);653 params.append('box_y1', boxCoords.y1);654 params.append('box_x2', boxCoords.x2);655 params.append('box_y2', boxCoords.y2);656 }657 658 // Добавляем маску если есть рисунок659 if (hasDrawing) {660 const maskData = drawCanvas.toDataURL('image/png');661 params.append('mask_data', maskData);662 }663 664 const url = `${API_URL}/segment?${params}`;665 console.log('Request URL:', url);666 667 const response = await fetch(url, {668 method: 'POST',669 body: formData670 });671 672 if (!response.ok) {673 throw new Error(`HTTP ${response.status}: ${await response.text()}`);674 }675 676 const result = await response.json();677 console.log('Result:', result);678 679 if (result.success && result.segments.length > 0) {680 displayResult(result.segments[0]);681 } else {682 showError('Не удалось найти объект');683 }684 685 } catch (error) {686 console.error('Error:', error);687 showError(`Ошибка: ${error.message}`);688 } finally {689 document.getElementById('loading').classList.remove('active');690 document.getElementById('segmentBtn').disabled = false;691 }692 }693 694 function isCanvasEmpty(canvas) {695 const ctx = canvas.getContext('2d');696 const pixelData = ctx.getImageData(0, 0, canvas.width, canvas.height).data;697 698 for (let i = 3; i < pixelData.length; i += 4) {699 if (pixelData[i] !== 0) return false;700 }701 return true;702 }703 704 function displayResult(segment) {705 if (!segment.extracted_image) {706 showError('Не получен вырезанный объект');707 return;708 }709 710 const img = new Image();711 img.onload = () => {712 resultCtx.clearRect(0, 0, resultCanvas.width, resultCanvas.height);713 714 const scale = Math.min(715 resultCanvas.width / img.width,716 resultCanvas.height / img.height,717 1718 );719 720 const w = img.width * scale;721 const h = img.height * scale;722 const x = (resultCanvas.width - w) / 2;723 const y = (resultCanvas.height - h) / 2;724 725 drawCheckerboard(resultCtx, x, y, w, h);726 resultCtx.drawImage(img, x, y, w, h);727 728 document.getElementById('resultArea').textContent = segment.area.toLocaleString();729 document.getElementById('resultBBox').textContent = 730 `${segment.bbox.width}×${segment.bbox.height} px`;731 document.getElementById('resultConfidence').textContent = 732 `${(segment.confidence * 100).toFixed(1)}%`;733 document.getElementById('resultInfo').style.display = 'block';734 };735 img.src = segment.extracted_image;736 }737 738 function drawCheckerboard(ctx, x, y, w, h) {739 const size = 10;740 ctx.fillStyle = '#f0f0f0';741 ctx.fillRect(x, y, w, h);742 743 ctx.fillStyle = '#ddd';744 for (let i = 0; i < w; i += size) {745 for (let j = 0; j < h; j += size) {746 if ((i / size + j / size) % 2 === 0) {747 ctx.fillRect(x + i, y + j, size, size);748 }749 }750 }751 }752 753 function showError(message) {754 const errorEl = document.getElementById('error');755 errorEl.textContent = '❌ ' + message;756 errorEl.classList.add('active');757 }758 759 // Инициализация760 setTool('box');761 </script>762</body>763</html>764 765 