sam2-api / web_demo_advanced.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 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>