sam2-api / web_demo.html
gbreadman13code
Deploy SAM2 segmentation API
4f2b4bb
<!DOCTYPE html>
<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>