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 Box Prompt Demo</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: 1400px;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 margin-bottom: 20px;47 }48 49 .upload-section {50 text-align: center;51 padding: 40px;52 border: 3px dashed #667eea;53 border-radius: 12px;54 background: #f8f9ff;55 cursor: pointer;56 transition: all 0.3s;57 }58 59 .upload-section:hover {60 background: #f0f1ff;61 border-color: #764ba2;62 }63 64 .upload-section.dragover {65 background: #e8e9ff;66 border-color: #764ba2;67 transform: scale(1.02);68 }69 70 input[type="file"] {71 display: none;72 }73 74 .upload-btn {75 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);76 color: white;77 border: none;78 padding: 15px 40px;79 font-size: 1.1em;80 border-radius: 8px;81 cursor: pointer;82 transition: transform 0.2s;83 font-weight: 600;84 }85 86 .upload-btn:hover {87 transform: translateY(-2px);88 box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);89 }90 91 .workspace {92 display: none;93 margin-top: 30px;94 }95 96 .workspace.active {97 display: grid;98 grid-template-columns: 1fr 1fr;99 gap: 30px;100 }101 102 .canvas-container {103 position: relative;104 background: #f8f9ff;105 border-radius: 12px;106 padding: 20px;107 box-shadow: inset 0 2px 8px rgba(0,0,0,0.1);108 }109 110 .canvas-title {111 font-size: 1.2em;112 font-weight: 600;113 margin-bottom: 15px;114 color: #333;115 }116 117 canvas {118 display: block;119 max-width: 100%;120 border-radius: 8px;121 box-shadow: 0 4px 12px rgba(0,0,0,0.15);122 cursor: crosshair;123 }124 125 .instructions {126 background: #fff3cd;127 border: 2px solid #ffc107;128 border-radius: 8px;129 padding: 15px;130 margin-bottom: 20px;131 font-size: 0.95em;132 line-height: 1.6;133 }134 135 .instructions strong {136 color: #856404;137 }138 139 .controls {140 display: flex;141 gap: 15px;142 margin-top: 20px;143 flex-wrap: wrap;144 }145 146 button {147 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);148 color: white;149 border: none;150 padding: 12px 30px;151 font-size: 1em;152 border-radius: 8px;153 cursor: pointer;154 transition: all 0.2s;155 font-weight: 600;156 }157 158 button:hover {159 transform: translateY(-2px);160 box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);161 }162 163 button:disabled {164 background: #ccc;165 cursor: not-allowed;166 transform: none;167 }168 169 button.clear {170 background: #dc3545;171 }172 173 button.clear:hover {174 box-shadow: 0 5px 15px rgba(220, 53, 69, 0.4);175 }176 177 .loading {178 display: none;179 text-align: center;180 padding: 30px;181 }182 183 .loading.active {184 display: block;185 }186 187 .spinner {188 border: 4px solid #f3f3f3;189 border-top: 4px solid #667eea;190 border-radius: 50%;191 width: 50px;192 height: 50px;193 animation: spin 1s linear infinite;194 margin: 0 auto 15px;195 }196 197 @keyframes spin {198 0% { transform: rotate(0deg); }199 100% { transform: rotate(360deg); }200 }201 202 .result-info {203 background: #d1ecf1;204 border: 2px solid #bee5eb;205 border-radius: 8px;206 padding: 15px;207 margin-top: 15px;208 font-size: 0.9em;209 }210 211 .result-info h4 {212 margin-bottom: 10px;213 color: #0c5460;214 }215 216 .result-info p {217 margin: 5px 0;218 color: #0c5460;219 }220 221 .error {222 background: #f8d7da;223 border: 2px solid #f5c6cb;224 color: #721c24;225 padding: 15px;226 border-radius: 8px;227 margin-top: 15px;228 display: none;229 }230 231 .error.active {232 display: block;233 }234 235 @media (max-width: 1200px) {236 .workspace.active {237 grid-template-columns: 1fr;238 }239 }240 241 .badge {242 display: inline-block;243 background: #28a745;244 color: white;245 padding: 4px 12px;246 border-radius: 12px;247 font-size: 0.85em;248 font-weight: 600;249 margin-left: 10px;250 }251 </style>252</head>253<body>254 <div class="container">255 <h1>🎯 SAM2 Box Prompt Demo</h1>256 <p class="subtitle">Выдели объект прямоугольником → Получи точную сегментацию</p>257 258 <div class="panel">259 <div class="upload-section" id="uploadSection">260 <h2 style="margin-bottom: 15px;">📸 Загрузи изображение</h2>261 <p style="color: #666; margin-bottom: 20px;">Кликни или перетащи фото сюда</p>262 <button class="upload-btn" onclick="document.getElementById('fileInput').click()">263 Выбрать файл264 </button>265 <input type="file" id="fileInput" accept="image/*">266 </div>267 268 <div class="workspace" id="workspace">269 <div class="instructions">270 <strong>📝 Как пользоваться:</strong><br>271 1. Зажми левую кнопку мыши и нарисуй прямоугольник вокруг объекта<br>272 2. Нажми "Сегментировать" чтобы получить результат<br>273 3. Справа увидишь вырезанный объект с прозрачностью274 </div>275 276 <div>277 <div class="canvas-container">278 <div class="canvas-title">279 Исходное изображение 280 <span class="badge" id="imageSizeBadge"></span>281 </div>282 <canvas id="sourceCanvas"></canvas>283 </div>284 285 <div class="controls">286 <button id="segmentBtn" onclick="segmentImage()">287 🚀 Сегментировать288 </button>289 <button class="clear" onclick="clearBox()">290 🗑️ Очистить выделение291 </button>292 <button class="clear" onclick="resetAll()">293 🔄 Новое изображение294 </button>295 </div>296 </div>297 298 <div>299 <div class="canvas-container">300 <div class="canvas-title">Результат (вырезанный объект)</div>301 <canvas id="resultCanvas"></canvas>302 </div>303 304 <div class="loading" id="loading">305 <div class="spinner"></div>306 <p>Обрабатываю изображение...</p>307 </div>308 309 <div class="result-info" id="resultInfo" style="display: none;">310 <h4>📊 Информация о сегменте:</h4>311 <p><strong>Площадь:</strong> <span id="resultArea"></span> пикселей</p>312 <p><strong>BBox:</strong> <span id="resultBBox"></span></p>313 <p><strong>Confidence:</strong> <span id="resultConfidence"></span></p>314 </div>315 316 <div class="error" id="error"></div>317 </div>318 </div>319 </div>320 </div>321 322 <script>323 const API_URL = 'http://localhost:8000';324 325 let sourceCanvas = document.getElementById('sourceCanvas');326 let resultCanvas = document.getElementById('resultCanvas');327 let sourceCtx = sourceCanvas.getContext('2d');328 let resultCtx = resultCanvas.getContext('2d');329 330 let currentImage = null;331 let isDrawing = false;332 let startX, startY, endX, endY;333 let boxCoords = null;334 335 // Drag & Drop336 const uploadSection = document.getElementById('uploadSection');337 338 uploadSection.addEventListener('dragover', (e) => {339 e.preventDefault();340 uploadSection.classList.add('dragover');341 });342 343 uploadSection.addEventListener('dragleave', () => {344 uploadSection.classList.remove('dragover');345 });346 347 uploadSection.addEventListener('drop', (e) => {348 e.preventDefault();349 uploadSection.classList.remove('dragover');350 const file = e.dataTransfer.files[0];351 if (file && file.type.startsWith('image/')) {352 loadImage(file);353 }354 });355 356 // File input357 document.getElementById('fileInput').addEventListener('change', (e) => {358 const file = e.target.files[0];359 if (file) {360 loadImage(file);361 }362 });363 364 function loadImage(file) {365 const reader = new FileReader();366 reader.onload = (e) => {367 const img = new Image();368 img.onload = () => {369 currentImage = img;370 setupCanvas(img);371 document.getElementById('workspace').classList.add('active');372 document.getElementById('imageSizeBadge').textContent = `${img.width}×${img.height}`;373 };374 img.src = e.target.result;375 };376 reader.readAsDataURL(file);377 }378 379 function setupCanvas(img) {380 const maxWidth = 600;381 const scale = Math.min(1, maxWidth / img.width);382 383 sourceCanvas.width = img.width * scale;384 sourceCanvas.height = img.height * scale;385 resultCanvas.width = sourceCanvas.width;386 resultCanvas.height = sourceCanvas.height;387 388 sourceCtx.drawImage(img, 0, 0, sourceCanvas.width, sourceCanvas.height);389 resultCtx.clearRect(0, 0, resultCanvas.width, resultCanvas.height);390 391 boxCoords = null;392 document.getElementById('resultInfo').style.display = 'none';393 document.getElementById('error').classList.remove('active');394 }395 396 // Рисование бокса397 sourceCanvas.addEventListener('mousedown', (e) => {398 const rect = sourceCanvas.getBoundingClientRect();399 startX = e.clientX - rect.left;400 startY = e.clientY - rect.top;401 isDrawing = true;402 });403 404 sourceCanvas.addEventListener('mousemove', (e) => {405 if (!isDrawing) return;406 407 const rect = sourceCanvas.getBoundingClientRect();408 endX = e.clientX - rect.left;409 endY = e.clientY - rect.top;410 411 redrawCanvas();412 drawBox(startX, startY, endX, endY);413 });414 415 sourceCanvas.addEventListener('mouseup', (e) => {416 if (!isDrawing) return;417 418 const rect = sourceCanvas.getBoundingClientRect();419 endX = e.clientX - rect.left;420 endY = e.clientY - rect.top;421 isDrawing = false;422 423 // Сохраняем координаты в масштабе оригинального изображения424 const scaleX = currentImage.width / sourceCanvas.width;425 const scaleY = currentImage.height / sourceCanvas.height;426 427 boxCoords = {428 x1: Math.min(startX, endX) * scaleX,429 y1: Math.min(startY, endY) * scaleY,430 x2: Math.max(startX, endX) * scaleX,431 y2: Math.max(startY, endY) * scaleY432 };433 434 console.log('Box coordinates:', boxCoords);435 });436 437 function redrawCanvas() {438 sourceCtx.clearRect(0, 0, sourceCanvas.width, sourceCanvas.height);439 sourceCtx.drawImage(currentImage, 0, 0, sourceCanvas.width, sourceCanvas.height);440 }441 442 function drawBox(x1, y1, x2, y2) {443 const width = x2 - x1;444 const height = y2 - y1;445 446 sourceCtx.strokeStyle = '#00ff00';447 sourceCtx.lineWidth = 3;448 sourceCtx.setLineDash([10, 5]);449 sourceCtx.strokeRect(x1, y1, width, height);450 451 // Полупрозрачная заливка452 sourceCtx.fillStyle = 'rgba(0, 255, 0, 0.1)';453 sourceCtx.fillRect(x1, y1, width, height);454 455 sourceCtx.setLineDash([]);456 }457 458 function clearBox() {459 boxCoords = null;460 redrawCanvas();461 resultCtx.clearRect(0, 0, resultCanvas.width, resultCanvas.height);462 document.getElementById('resultInfo').style.display = 'none';463 }464 465 function resetAll() {466 currentImage = null;467 boxCoords = null;468 document.getElementById('workspace').classList.remove('active');469 document.getElementById('fileInput').value = '';470 document.getElementById('resultInfo').style.display = 'none';471 document.getElementById('error').classList.remove('active');472 }473 474 async function segmentImage() {475 if (!boxCoords) {476 showError('Сначала нарисуй прямоугольник на изображении!');477 return;478 }479 480 const { x1, y1, x2, y2 } = boxCoords;481 482 if (x2 - x1 < 10 || y2 - y1 < 10) {483 showError('Выделенная область слишком маленькая. Нарисуй больший прямоугольник.');484 return;485 }486 487 document.getElementById('loading').classList.add('active');488 document.getElementById('error').classList.remove('active');489 document.getElementById('segmentBtn').disabled = true;490 491 try {492 // Конвертируем изображение в blob493 const canvas = document.createElement('canvas');494 canvas.width = currentImage.width;495 canvas.height = currentImage.height;496 const ctx = canvas.getContext('2d');497 ctx.drawImage(currentImage, 0, 0);498 499 const blob = await new Promise(resolve => canvas.toBlob(resolve, 'image/jpeg', 0.95));500 501 // Формируем запрос502 const formData = new FormData();503 formData.append('file', blob, 'image.jpg');504 505 const url = `${API_URL}/segment?box_x1=${x1}&box_y1=${y1}&box_x2=${x2}&box_y2=${y2}&extract_objects=true&include_masks=false`;506 507 console.log('Request URL:', url);508 509 const response = await fetch(url, {510 method: 'POST',511 body: formData512 });513 514 if (!response.ok) {515 throw new Error(`HTTP ${response.status}: ${await response.text()}`);516 }517 518 const result = await response.json();519 console.log('Result:', result);520 521 if (result.success && result.segments.length > 0) {522 displayResult(result.segments[0]);523 } else {524 showError('Не удалось найти объект в выделенной области');525 }526 527 } catch (error) {528 console.error('Error:', error);529 showError(`Ошибка: ${error.message}`);530 } finally {531 document.getElementById('loading').classList.remove('active');532 document.getElementById('segmentBtn').disabled = false;533 }534 }535 536 function displayResult(segment) {537 if (!segment.extracted_image) {538 showError('Не получен вырезанный объект');539 return;540 }541 542 const img = new Image();543 img.onload = () => {544 resultCtx.clearRect(0, 0, resultCanvas.width, resultCanvas.height);545 546 // Рисуем в центре canvas547 const scale = Math.min(548 resultCanvas.width / img.width,549 resultCanvas.height / img.height,550 1551 );552 553 const w = img.width * scale;554 const h = img.height * scale;555 const x = (resultCanvas.width - w) / 2;556 const y = (resultCanvas.height - h) / 2;557 558 // Клетчатый фон для прозрачности559 drawCheckerboard(resultCtx, x, y, w, h);560 resultCtx.drawImage(img, x, y, w, h);561 562 // Показываем инфу563 document.getElementById('resultArea').textContent = segment.area.toLocaleString();564 document.getElementById('resultBBox').textContent = 565 `${segment.bbox.width}×${segment.bbox.height} px`;566 document.getElementById('resultConfidence').textContent = 567 `${(segment.confidence * 100).toFixed(1)}%`;568 document.getElementById('resultInfo').style.display = 'block';569 };570 img.src = segment.extracted_image;571 }572 573 function drawCheckerboard(ctx, x, y, w, h) {574 const size = 10;575 ctx.fillStyle = '#f0f0f0';576 ctx.fillRect(x, y, w, h);577 578 ctx.fillStyle = '#ddd';579 for (let i = 0; i < w; i += size) {580 for (let j = 0; j < h; j += size) {581 if ((i / size + j / size) % 2 === 0) {582 ctx.fillRect(x + i, y + j, size, size);583 }584 }585 }586 }587 588 function showError(message) {589 const errorEl = document.getElementById('error');590 errorEl.textContent = '❌ ' + message;591 errorEl.classList.add('active');592 }593 </script>594</body>595</html>596 597 