|
|
<!DOCTYPE html> |
|
|
<html lang="es"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<title>YOLO Annotator - Autenticado</title> |
|
|
<style> |
|
|
* { |
|
|
margin: 0; |
|
|
padding: 0; |
|
|
box-sizing: border-box; |
|
|
} |
|
|
|
|
|
body { |
|
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; |
|
|
background-color: #f5f5f5; |
|
|
min-height: 100vh; |
|
|
} |
|
|
|
|
|
.header { |
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
|
|
color: white; |
|
|
padding: 1rem 2rem; |
|
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); |
|
|
} |
|
|
|
|
|
.header-content { |
|
|
display: flex; |
|
|
justify-content: space-between; |
|
|
align-items: center; |
|
|
max-width: 1200px; |
|
|
margin: 0 auto; |
|
|
} |
|
|
|
|
|
.back-btn { |
|
|
color: white; |
|
|
text-decoration: none; |
|
|
padding: 0.5rem 1rem; |
|
|
background: rgba(255, 255, 255, 0.2); |
|
|
border-radius: 5px; |
|
|
transition: background 0.3s; |
|
|
} |
|
|
|
|
|
.back-btn:hover { |
|
|
background: rgba(255, 255, 255, 0.3); |
|
|
} |
|
|
|
|
|
.header-left { |
|
|
display: flex; |
|
|
align-items: center; |
|
|
gap: 1rem; |
|
|
} |
|
|
|
|
|
.user-info { |
|
|
display: flex; |
|
|
align-items: center; |
|
|
gap: 1rem; |
|
|
} |
|
|
|
|
|
.user-badge { |
|
|
background: rgba(255, 255, 255, 0.2); |
|
|
padding: 0.5rem 1rem; |
|
|
border-radius: 20px; |
|
|
font-size: 0.9rem; |
|
|
} |
|
|
|
|
|
.admin-badge { |
|
|
background: #ff6b6b; |
|
|
color: white; |
|
|
padding: 0.2rem 0.5rem; |
|
|
border-radius: 10px; |
|
|
font-size: 0.7rem; |
|
|
margin-left: 0.5rem; |
|
|
} |
|
|
|
|
|
.logout-btn { |
|
|
background: rgba(255, 255, 255, 0.1); |
|
|
color: white; |
|
|
border: 1px solid rgba(255, 255, 255, 0.3); |
|
|
padding: 0.5rem 1rem; |
|
|
border-radius: 5px; |
|
|
cursor: pointer; |
|
|
transition: all 0.3s; |
|
|
} |
|
|
|
|
|
.logout-btn:hover { |
|
|
background: rgba(255, 255, 255, 0.2); |
|
|
} |
|
|
|
|
|
.container { |
|
|
max-width: 1200px; |
|
|
margin: 0 auto; |
|
|
padding: 2rem; |
|
|
} |
|
|
|
|
|
.main-panel { |
|
|
background: white; |
|
|
border-radius: 10px; |
|
|
padding: 2rem; |
|
|
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); |
|
|
margin-bottom: 2rem; |
|
|
} |
|
|
|
|
|
.control-group { |
|
|
margin-bottom: 1rem; |
|
|
} |
|
|
|
|
|
.control-group label { |
|
|
display: block; |
|
|
margin-bottom: 0.5rem; |
|
|
color: #333; |
|
|
font-weight: 500; |
|
|
} |
|
|
|
|
|
.control-group input, |
|
|
.control-group select { |
|
|
width: 100%; |
|
|
max-width: 300px; |
|
|
padding: 0.5rem; |
|
|
border: 1px solid #ddd; |
|
|
border-radius: 5px; |
|
|
} |
|
|
|
|
|
.btn { |
|
|
padding: 0.75rem 1.5rem; |
|
|
background: #667eea; |
|
|
color: white; |
|
|
border: none; |
|
|
border-radius: 5px; |
|
|
cursor: pointer; |
|
|
font-size: 1rem; |
|
|
margin: 0.5rem 0.5rem 0.5rem 0; |
|
|
} |
|
|
|
|
|
.btn:hover { |
|
|
opacity: 0.9; |
|
|
} |
|
|
|
|
|
.btn-success { |
|
|
background: #28a745; |
|
|
} |
|
|
|
|
|
.btn-danger { |
|
|
background: #dc3545; |
|
|
} |
|
|
|
|
|
.alert { |
|
|
padding: 1rem; |
|
|
border-radius: 5px; |
|
|
margin-bottom: 1rem; |
|
|
} |
|
|
|
|
|
.alert-success { |
|
|
background: #d4edda; |
|
|
color: #155724; |
|
|
border: 1px solid #c3e6cb; |
|
|
} |
|
|
|
|
|
.alert-error { |
|
|
background: #f8d7da; |
|
|
color: #721c24; |
|
|
border: 1px solid #f5c6cb; |
|
|
} |
|
|
|
|
|
.alert-info { |
|
|
background: #e6f3ff; |
|
|
color: #0066cc; |
|
|
border: 1px solid #b3d9ff; |
|
|
} |
|
|
|
|
|
.canvas-container { |
|
|
text-align: center; |
|
|
margin: 2rem 0; |
|
|
} |
|
|
|
|
|
.canvas-container canvas { |
|
|
border: 2px solid #ddd; |
|
|
border-radius: 5px; |
|
|
max-width: 100%; |
|
|
cursor: crosshair; |
|
|
} |
|
|
|
|
|
.annotation-controls { |
|
|
background: #f8f9fa; |
|
|
border-radius: 10px; |
|
|
padding: 1.5rem; |
|
|
margin-top: 2rem; |
|
|
} |
|
|
|
|
|
.class-buttons { |
|
|
display: flex; |
|
|
gap: 0.5rem; |
|
|
flex-wrap: wrap; |
|
|
margin-bottom: 1rem; |
|
|
} |
|
|
|
|
|
.class-btn { |
|
|
padding: 0.5rem 1rem; |
|
|
border: 2px solid; |
|
|
border-radius: 5px; |
|
|
cursor: pointer; |
|
|
font-size: 0.9rem; |
|
|
transition: all 0.3s; |
|
|
} |
|
|
|
|
|
.class-btn.active { |
|
|
transform: scale(1.1); |
|
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.3); |
|
|
} |
|
|
|
|
|
.annotations-list { |
|
|
background: white; |
|
|
border: 1px solid #ddd; |
|
|
border-radius: 5px; |
|
|
min-height: 100px; |
|
|
padding: 1rem; |
|
|
margin: 1rem 0; |
|
|
} |
|
|
|
|
|
.annotation-item { |
|
|
display: flex; |
|
|
justify-content: space-between; |
|
|
align-items: center; |
|
|
padding: 0.5rem; |
|
|
border-bottom: 1px solid #eee; |
|
|
} |
|
|
|
|
|
.annotation-item:last-child { |
|
|
border-bottom: none; |
|
|
} |
|
|
|
|
|
|
|
|
.modal { |
|
|
display: none; |
|
|
position: fixed; |
|
|
z-index: 1000; |
|
|
left: 0; |
|
|
top: 0; |
|
|
width: 100%; |
|
|
height: 100%; |
|
|
background-color: rgba(0,0,0,0.5); |
|
|
} |
|
|
|
|
|
.modal-content { |
|
|
background-color: #fefefe; |
|
|
margin: 5% auto; |
|
|
padding: 20px; |
|
|
border-radius: 10px; |
|
|
width: 90%; |
|
|
max-width: 600px; |
|
|
max-height: 80vh; |
|
|
overflow-y: auto; |
|
|
} |
|
|
|
|
|
.modal-header { |
|
|
display: flex; |
|
|
justify-content: space-between; |
|
|
align-items: center; |
|
|
margin-bottom: 20px; |
|
|
padding-bottom: 10px; |
|
|
border-bottom: 1px solid #ddd; |
|
|
} |
|
|
|
|
|
.close { |
|
|
color: #aaa; |
|
|
font-size: 28px; |
|
|
font-weight: bold; |
|
|
cursor: pointer; |
|
|
} |
|
|
|
|
|
.close:hover { |
|
|
color: #000; |
|
|
} |
|
|
|
|
|
.class-form { |
|
|
display: flex; |
|
|
gap: 10px; |
|
|
margin-bottom: 20px; |
|
|
padding: 15px; |
|
|
background: #f8f9fa; |
|
|
border-radius: 8px; |
|
|
} |
|
|
|
|
|
.class-form input { |
|
|
flex: 1; |
|
|
padding: 8px; |
|
|
border: 1px solid #ddd; |
|
|
border-radius: 4px; |
|
|
} |
|
|
|
|
|
.color-picker { |
|
|
width: 50px; |
|
|
height: 38px; |
|
|
border: 1px solid #ddd; |
|
|
border-radius: 4px; |
|
|
cursor: pointer; |
|
|
} |
|
|
|
|
|
.class-list { |
|
|
max-height: 300px; |
|
|
overflow-y: auto; |
|
|
} |
|
|
|
|
|
.class-item-manager { |
|
|
display: flex; |
|
|
align-items: center; |
|
|
gap: 10px; |
|
|
padding: 10px; |
|
|
border: 1px solid #ddd; |
|
|
border-radius: 5px; |
|
|
margin-bottom: 5px; |
|
|
background: white; |
|
|
} |
|
|
|
|
|
.class-color-preview { |
|
|
width: 30px; |
|
|
height: 30px; |
|
|
border-radius: 50%; |
|
|
border: 2px solid #ddd; |
|
|
} |
|
|
|
|
|
.class-info { |
|
|
flex: 1; |
|
|
} |
|
|
|
|
|
.class-actions { |
|
|
display: flex; |
|
|
gap: 5px; |
|
|
} |
|
|
|
|
|
.btn-small { |
|
|
padding: 5px 10px; |
|
|
font-size: 12px; |
|
|
border: none; |
|
|
border-radius: 3px; |
|
|
cursor: pointer; |
|
|
} |
|
|
|
|
|
.btn-edit { |
|
|
background: #007bff; |
|
|
color: white; |
|
|
} |
|
|
|
|
|
.btn-delete { |
|
|
background: #dc3545; |
|
|
color: white; |
|
|
} |
|
|
|
|
|
.preset-colors { |
|
|
display: grid; |
|
|
grid-template-columns: repeat(6, 1fr); |
|
|
gap: 5px; |
|
|
margin: 10px 0; |
|
|
} |
|
|
|
|
|
.preset-color { |
|
|
width: 30px; |
|
|
height: 30px; |
|
|
border-radius: 50%; |
|
|
border: 2px solid #ddd; |
|
|
cursor: pointer; |
|
|
transition: transform 0.2s; |
|
|
} |
|
|
|
|
|
.preset-color:hover { |
|
|
transform: scale(1.1); |
|
|
border-color: #333; |
|
|
} |
|
|
|
|
|
@media (max-width: 768px) { |
|
|
.header-content { |
|
|
flex-direction: column; |
|
|
gap: 1rem; |
|
|
} |
|
|
|
|
|
.container { |
|
|
padding: 1rem; |
|
|
} |
|
|
|
|
|
.class-buttons { |
|
|
justify-content: center; |
|
|
} |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<header class="header"> |
|
|
<div class="header-content"> |
|
|
<div class="header-left"> |
|
|
<a href="/dashboard" class="back-btn">← Dashboard</a> |
|
|
<h1>🏷️ YOLO Annotator</h1> |
|
|
</div> |
|
|
<div class="user-info"> |
|
|
{% if user %} |
|
|
<div class="user-badge"> |
|
|
👤 {{ user.username }} |
|
|
{% if user and user.is_admin %} |
|
|
<span class="admin-badge">ADMIN</span> |
|
|
{% endif %} |
|
|
</div> |
|
|
<button class="logout-btn" onclick="logout()">Cerrar Sesión</button> |
|
|
{% else %} |
|
|
<div class="user-badge"> |
|
|
👤 Verificando autenticación... |
|
|
</div> |
|
|
{% endif %} |
|
|
</div> |
|
|
</div> |
|
|
</header> |
|
|
|
|
|
<div class="container"> |
|
|
<div id="alert-container"></div> |
|
|
|
|
|
<div class="alert alert-info"> |
|
|
📝 <strong>Instrucciones:</strong> Sube una imagen, configura parámetros, genera el canvas, selecciona una clase y arrastra para crear bounding boxes. |
|
|
</div> |
|
|
|
|
|
<div class="main-panel"> |
|
|
<h2>📤 Subir y Configurar Imagen</h2> |
|
|
|
|
|
<form id="uploadForm"> |
|
|
<div class="control-group"> |
|
|
<label for="sessionSelect">Sesión:</label> |
|
|
<select id="sessionSelect" required> |
|
|
<option value="">Selecciona una sesión...</option> |
|
|
</select> |
|
|
</div> |
|
|
|
|
|
<div class="control-group"> |
|
|
<label for="imageFile">Imagen:</label> |
|
|
<input type="file" id="imageFile" accept="image/*" required> |
|
|
<small style="color: #666; display: block; margin-top: 0.25rem;"> |
|
|
Formatos soportados: JPG, PNG, WebP, GIF, BMP |
|
|
</small> |
|
|
</div> |
|
|
|
|
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem;"> |
|
|
<div class="control-group"> |
|
|
<label for="canvasWidth">Ancho Canvas:</label> |
|
|
<select id="canvasWidth"> |
|
|
<option value="640">640px</option> |
|
|
<option value="320">320px</option> |
|
|
<option value="800">800px</option> |
|
|
</select> |
|
|
</div> |
|
|
|
|
|
<div class="control-group"> |
|
|
<label for="canvasHeight">Alto Canvas:</label> |
|
|
<select id="canvasHeight"> |
|
|
<option value="640">640px</option> |
|
|
<option value="320">320px</option> |
|
|
<option value="800">800px</option> |
|
|
</select> |
|
|
</div> |
|
|
|
|
|
<div class="control-group"> |
|
|
<label for="imagePosX">Posición X:</label> |
|
|
<input type="number" id="imagePosX" value="0" min="0"> |
|
|
</div> |
|
|
|
|
|
<div class="control-group"> |
|
|
<label for="imagePosY">Posición Y:</label> |
|
|
<input type="number" id="imagePosY" value="0" min="0"> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="control-group"> |
|
|
<label> |
|
|
<input type="checkbox" id="changeBg" checked> Fondo aleatorio |
|
|
</label> |
|
|
</div> |
|
|
|
|
|
<button type="submit" class="btn">🖼️ Generar Canvas</button> |
|
|
</form> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="canvas-container" id="canvasContainer" style="display: none;"> |
|
|
<canvas id="imageCanvas"></canvas> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="annotation-controls" id="annotationControls" style="display: none;"> |
|
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;"> |
|
|
<h3>🎯 Seleccionar Clase para Anotar</h3> |
|
|
<button onclick="openClassManager()" class="btn" style="background: #28a745;">⚙️ Gestionar Clases</button> |
|
|
</div> |
|
|
<div class="class-buttons" id="classButtons"> |
|
|
|
|
|
</div> |
|
|
|
|
|
<h3>📝 Anotaciones Actuales</h3> |
|
|
<div class="annotations-list" id="annotationsList"> |
|
|
<p style="text-align: center; color: #666;">Sin anotaciones aún. Selecciona una clase y arrastra en la imagen.</p> |
|
|
</div> |
|
|
|
|
|
<div style="display: flex; gap: 1rem; align-items: center; flex-wrap: wrap;"> |
|
|
<input type="text" id="filename" placeholder="Nombre del archivo (opcional)" style="flex: 1; min-width: 200px;"> |
|
|
<button onclick="saveAnnotations()" class="btn btn-success">💾 Guardar Anotaciones</button> |
|
|
<button onclick="clearAnnotations()" class="btn btn-danger">🗑️ Limpiar Todo</button> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div id="classManagerModal" class="modal"> |
|
|
<div class="modal-content"> |
|
|
<div class="modal-header"> |
|
|
<h2>⚙️ Gestión de Clases</h2> |
|
|
<span class="close" onclick="closeClassManager()">×</span> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="class-form"> |
|
|
<input type="text" id="className" placeholder="Nombre de la clase" maxlength="50"> |
|
|
<input type="color" id="classColor" class="color-picker" value="#ff0000"> |
|
|
<button onclick="saveClass()" class="btn btn-success">Guardar</button> |
|
|
<button onclick="cancelEdit()" class="btn" id="cancelEditBtn" style="display: none;">Cancelar</button> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div style="margin-bottom: 15px;"> |
|
|
<h4>🎨 Colores predefinidos:</h4> |
|
|
<div class="preset-colors" id="presetColors"> |
|
|
|
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div> |
|
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;"> |
|
|
<h4>📋 Clases existentes:</h4> |
|
|
<button onclick="resetToDefaultClasses()" class="btn" style="background: #6c757d;">🔄 Restablecer por defecto</button> |
|
|
</div> |
|
|
<div class="class-list" id="classList"> |
|
|
|
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div style="margin-top: 20px; padding-top: 15px; border-top: 1px solid #ddd;"> |
|
|
<button onclick="importClassesFromAnnotations()" class="btn" style="background: #17a2b8;">📥 Importar desde anotaciones</button> |
|
|
<button onclick="closeClassManager()" class="btn btn-danger">❌ Cerrar</button> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<script> |
|
|
|
|
|
let currentUser = null; |
|
|
let currentCanvas = null; |
|
|
let currentContext = null; |
|
|
let currentImage = null; |
|
|
let currentSession = null; |
|
|
let currentFilename = null; |
|
|
let annotations = []; |
|
|
let isDrawing = false; |
|
|
let startX, startY; |
|
|
let selectedClass = 0; |
|
|
let editingClassId = null; |
|
|
|
|
|
|
|
|
let classes = [ |
|
|
{ id: 0, name: 'Clase 0', color: '#ff0000' }, |
|
|
{ id: 1, name: 'Clase 1', color: '#00ff00' }, |
|
|
{ id: 2, name: 'Clase 2', color: '#0000ff' }, |
|
|
{ id: 3, name: 'Clase 3', color: '#ffff00' }, |
|
|
{ id: 4, name: 'Clase 4', color: '#ff00ff' }, |
|
|
{ id: 5, name: 'Clase 5', color: '#00ffff' } |
|
|
]; |
|
|
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', function() { |
|
|
loadUserClasses(); |
|
|
loadPresetColors(); |
|
|
}); |
|
|
|
|
|
function getAuthHeaders() { |
|
|
const token = localStorage.getItem('access_token'); |
|
|
const tokenType = localStorage.getItem('token_type') || 'bearer'; |
|
|
|
|
|
if (!token) { |
|
|
window.location.href = '/login'; |
|
|
return {}; |
|
|
} |
|
|
|
|
|
return { |
|
|
'Authorization': `Bearer ${token}` |
|
|
}; |
|
|
} |
|
|
|
|
|
function showAlert(message, type = 'info') { |
|
|
const alertContainer = document.getElementById('alert-container'); |
|
|
alertContainer.innerHTML = ` |
|
|
<div class="alert alert-${type}"> |
|
|
${message} |
|
|
</div> |
|
|
`; |
|
|
|
|
|
setTimeout(() => { |
|
|
alertContainer.innerHTML = ''; |
|
|
}, 5000); |
|
|
} |
|
|
|
|
|
function getAuthHeaders() { |
|
|
const token = localStorage.getItem('access_token'); |
|
|
const tokenType = localStorage.getItem('token_type') || 'bearer'; |
|
|
|
|
|
if (!token) { |
|
|
window.location.href = '/login'; |
|
|
return {}; |
|
|
} |
|
|
|
|
|
return { |
|
|
'Authorization': `Bearer ${token}` |
|
|
}; |
|
|
} |
|
|
|
|
|
async function loadSessions() { |
|
|
try { |
|
|
const response = await fetch('/api/sessions', { |
|
|
headers: getAuthHeaders() |
|
|
}); |
|
|
|
|
|
const data = await response.json(); |
|
|
|
|
|
if (data.success) { |
|
|
const sessionSelect = document.getElementById('sessionSelect'); |
|
|
sessionSelect.innerHTML = '<option value="">Selecciona una sesión...</option>'; |
|
|
|
|
|
data.sessions.forEach(session => { |
|
|
const option = document.createElement('option'); |
|
|
option.value = session.name; |
|
|
option.textContent = `${session.name} (${session.images_count} imgs, ${session.labels_count} labels)`; |
|
|
sessionSelect.appendChild(option); |
|
|
}); |
|
|
} |
|
|
} catch (error) { |
|
|
console.error('Error loading sessions:', error); |
|
|
showAlert('Error al cargar sesiones', 'error'); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function createClassButtons() { |
|
|
const container = document.getElementById('classButtons'); |
|
|
container.innerHTML = ''; |
|
|
|
|
|
classes.forEach(cls => { |
|
|
const button = document.createElement('button'); |
|
|
button.className = 'class-btn'; |
|
|
button.style.backgroundColor = cls.color; |
|
|
button.style.color = 'white'; |
|
|
button.style.borderColor = cls.color; |
|
|
button.textContent = cls.name; |
|
|
button.onclick = () => selectClass(cls.id); |
|
|
|
|
|
if (cls.id === selectedClass) { |
|
|
button.classList.add('active'); |
|
|
} |
|
|
|
|
|
container.appendChild(button); |
|
|
}); |
|
|
} |
|
|
|
|
|
function selectClass(classId) { |
|
|
selectedClass = classId; |
|
|
createClassButtons(); |
|
|
} |
|
|
|
|
|
|
|
|
document.getElementById('uploadForm').addEventListener('submit', async (e) => { |
|
|
e.preventDefault(); |
|
|
|
|
|
const sessionName = document.getElementById('sessionSelect').value; |
|
|
const imageFile = document.getElementById('imageFile').files[0]; |
|
|
const canvasWidth = parseInt(document.getElementById('canvasWidth').value); |
|
|
const canvasHeight = parseInt(document.getElementById('canvasHeight').value); |
|
|
const x = parseInt(document.getElementById('imagePosX').value); |
|
|
const y = parseInt(document.getElementById('imagePosY').value); |
|
|
const changeBg = document.getElementById('changeBg').checked; |
|
|
|
|
|
if (!sessionName || !imageFile) { |
|
|
showAlert('Por favor selecciona una sesión y una imagen', 'error'); |
|
|
return; |
|
|
} |
|
|
|
|
|
const formData = new FormData(); |
|
|
formData.append('session', sessionName); |
|
|
formData.append('file', imageFile); |
|
|
formData.append('canvas_width', canvasWidth); |
|
|
formData.append('canvas_height', canvasHeight); |
|
|
formData.append('x', x); |
|
|
formData.append('y', y); |
|
|
formData.append('change_bg', changeBg); |
|
|
|
|
|
try { |
|
|
const response = await fetch('/api/upload', { |
|
|
method: 'POST', |
|
|
headers: getAuthHeaders(), |
|
|
body: formData |
|
|
}); |
|
|
|
|
|
const data = await response.json(); |
|
|
|
|
|
if (data.success) { |
|
|
showAlert('Imagen cargada exitosamente', 'success'); |
|
|
currentSession = sessionName; |
|
|
currentFilename = data.filename; |
|
|
document.getElementById('filename').value = data.filename; |
|
|
|
|
|
|
|
|
loadImageToCanvas(data.preview, canvasWidth, canvasHeight); |
|
|
|
|
|
|
|
|
document.getElementById('canvasContainer').style.display = 'block'; |
|
|
document.getElementById('annotationControls').style.display = 'block'; |
|
|
|
|
|
|
|
|
createClassButtons(); |
|
|
|
|
|
} else { |
|
|
showAlert(data.message || 'Error al cargar imagen', 'error'); |
|
|
} |
|
|
} catch (error) { |
|
|
console.error('Error:', error); |
|
|
showAlert('Error de conexión', 'error'); |
|
|
} |
|
|
}); |
|
|
|
|
|
function loadImageToCanvas(imageSrc, width, height) { |
|
|
const canvas = document.getElementById('imageCanvas'); |
|
|
const ctx = canvas.getContext('2d'); |
|
|
|
|
|
canvas.width = width; |
|
|
canvas.height = height; |
|
|
|
|
|
const img = new Image(); |
|
|
img.onload = function() { |
|
|
ctx.clearRect(0, 0, width, height); |
|
|
ctx.drawImage(img, 0, 0); |
|
|
|
|
|
currentCanvas = canvas; |
|
|
currentContext = ctx; |
|
|
currentImage = img; |
|
|
|
|
|
|
|
|
setupCanvasEvents(); |
|
|
}; |
|
|
img.src = imageSrc; |
|
|
} |
|
|
|
|
|
function setupCanvasEvents() { |
|
|
currentCanvas.addEventListener('mousedown', startDrawing); |
|
|
currentCanvas.addEventListener('mousemove', draw); |
|
|
currentCanvas.addEventListener('mouseup', stopDrawing); |
|
|
} |
|
|
|
|
|
function startDrawing(e) { |
|
|
isDrawing = true; |
|
|
const rect = currentCanvas.getBoundingClientRect(); |
|
|
startX = e.clientX - rect.left; |
|
|
startY = e.clientY - rect.top; |
|
|
} |
|
|
|
|
|
function draw(e) { |
|
|
if (!isDrawing) return; |
|
|
|
|
|
const rect = currentCanvas.getBoundingClientRect(); |
|
|
const currentX = e.clientX - rect.left; |
|
|
const currentY = e.clientY - rect.top; |
|
|
|
|
|
|
|
|
redrawCanvas(); |
|
|
|
|
|
|
|
|
currentContext.strokeStyle = classes[selectedClass].color; |
|
|
currentContext.lineWidth = 2; |
|
|
currentContext.strokeRect(startX, startY, currentX - startX, currentY - startY); |
|
|
} |
|
|
|
|
|
function stopDrawing(e) { |
|
|
if (!isDrawing) return; |
|
|
isDrawing = false; |
|
|
|
|
|
const rect = currentCanvas.getBoundingClientRect(); |
|
|
const endX = e.clientX - rect.left; |
|
|
const endY = e.clientY - rect.top; |
|
|
|
|
|
|
|
|
const x1 = Math.min(startX, endX); |
|
|
const y1 = Math.min(startY, endY); |
|
|
const x2 = Math.max(startX, endX); |
|
|
const y2 = Math.max(startY, endY); |
|
|
|
|
|
const width = x2 - x1; |
|
|
const height = y2 - y1; |
|
|
|
|
|
|
|
|
if (width > 5 && height > 5) { |
|
|
const canvasWidth = currentCanvas.width; |
|
|
const canvasHeight = currentCanvas.height; |
|
|
|
|
|
const annotation = { |
|
|
class_id: selectedClass, |
|
|
x_center: (x1 + width/2) / canvasWidth, |
|
|
y_center: (y1 + height/2) / canvasHeight, |
|
|
width: width / canvasWidth, |
|
|
height: height / canvasHeight, |
|
|
x1: x1, |
|
|
y1: y1, |
|
|
x2: x2, |
|
|
y2: y2 |
|
|
}; |
|
|
|
|
|
annotations.push(annotation); |
|
|
updateAnnotationsList(); |
|
|
redrawCanvas(); |
|
|
} |
|
|
} |
|
|
|
|
|
function redrawCanvas() { |
|
|
if (!currentImage) return; |
|
|
|
|
|
currentContext.clearRect(0, 0, currentCanvas.width, currentCanvas.height); |
|
|
currentContext.drawImage(currentImage, 0, 0); |
|
|
|
|
|
|
|
|
annotations.forEach(ann => { |
|
|
currentContext.strokeStyle = classes[ann.class_id].color; |
|
|
currentContext.lineWidth = 2; |
|
|
currentContext.strokeRect(ann.x1, ann.y1, ann.x2 - ann.x1, ann.y2 - ann.y1); |
|
|
|
|
|
|
|
|
currentContext.fillStyle = classes[ann.class_id].color; |
|
|
currentContext.fillRect(ann.x1, ann.y1 - 20, 60, 20); |
|
|
currentContext.fillStyle = 'white'; |
|
|
currentContext.font = '12px Arial'; |
|
|
currentContext.fillText(classes[ann.class_id].name, ann.x1 + 5, ann.y1 - 5); |
|
|
}); |
|
|
} |
|
|
|
|
|
function updateAnnotationsList() { |
|
|
const container = document.getElementById('annotationsList'); |
|
|
|
|
|
if (annotations.length === 0) { |
|
|
container.innerHTML = '<p style="text-align: center; color: #666;">Sin anotaciones aún. Selecciona una clase y arrastra en la imagen.</p>'; |
|
|
return; |
|
|
} |
|
|
|
|
|
container.innerHTML = annotations.map((ann, index) => ` |
|
|
<div class="annotation-item"> |
|
|
<span style="color: ${classes[ann.class_id].color};"> |
|
|
${classes[ann.class_id].name} - x:${ann.x_center.toFixed(3)}, y:${ann.y_center.toFixed(3)}, w:${ann.width.toFixed(3)}, h:${ann.height.toFixed(3)} |
|
|
</span> |
|
|
<button onclick="removeAnnotation(${index})" class="btn btn-danger" style="padding: 0.25rem 0.5rem; font-size: 0.8rem;">✖</button> |
|
|
</div> |
|
|
`).join(''); |
|
|
} |
|
|
|
|
|
function removeAnnotation(index) { |
|
|
annotations.splice(index, 1); |
|
|
updateAnnotationsList(); |
|
|
redrawCanvas(); |
|
|
} |
|
|
|
|
|
function clearAnnotations() { |
|
|
annotations = []; |
|
|
updateAnnotationsList(); |
|
|
redrawCanvas(); |
|
|
} |
|
|
|
|
|
async function saveAnnotations() { |
|
|
if (!currentSession || !currentFilename) { |
|
|
showAlert('No hay imagen cargada para guardar', 'error'); |
|
|
return; |
|
|
} |
|
|
|
|
|
if (annotations.length === 0) { |
|
|
showAlert('No hay anotaciones para guardar', 'error'); |
|
|
return; |
|
|
} |
|
|
|
|
|
const filename = document.getElementById('filename').value || currentFilename; |
|
|
|
|
|
const formData = new FormData(); |
|
|
formData.append('session', currentSession); |
|
|
formData.append('filename', filename); |
|
|
formData.append('annotations', JSON.stringify(annotations)); |
|
|
|
|
|
try { |
|
|
const response = await fetch('/api/save_annotations', { |
|
|
method: 'POST', |
|
|
headers: getAuthHeaders(), |
|
|
body: formData |
|
|
}); |
|
|
|
|
|
const data = await response.json(); |
|
|
|
|
|
if (data.success) { |
|
|
showAlert(`Anotaciones guardadas: ${data.annotations_count} anotaciones`, 'success'); |
|
|
} else { |
|
|
showAlert(data.message || 'Error al guardar anotaciones', 'error'); |
|
|
} |
|
|
} catch (error) { |
|
|
console.error('Error:', error); |
|
|
showAlert('Error de conexión al guardar', 'error'); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function loadUserClasses(sessionName = null) { |
|
|
try { |
|
|
const params = sessionName ? `?session_name=${encodeURIComponent(sessionName)}` : ''; |
|
|
const response = await fetch(`/api/classes/${params}`, { |
|
|
headers: getAuthHeaders() |
|
|
}); |
|
|
|
|
|
if (response.ok) { |
|
|
const userClasses = await response.json(); |
|
|
|
|
|
|
|
|
classes = userClasses.map((cls, index) => ({ |
|
|
id: index, |
|
|
dbId: cls.id, |
|
|
name: cls.name, |
|
|
color: cls.color |
|
|
})); |
|
|
|
|
|
createClassButtons(); |
|
|
updateClassList(); |
|
|
|
|
|
console.log(`📋 Cargadas ${classes.length} clases`); |
|
|
} else { |
|
|
console.error('Error cargando clases:', response.statusText); |
|
|
showAlert('Error cargando clases personalizadas', 'error'); |
|
|
} |
|
|
} catch (error) { |
|
|
console.error('Error:', error); |
|
|
showAlert('Error de conexión cargando clases', 'error'); |
|
|
} |
|
|
} |
|
|
|
|
|
async function loadPresetColors() { |
|
|
try { |
|
|
const response = await fetch('/api/classes/available-colors', { |
|
|
headers: getAuthHeaders() |
|
|
}); |
|
|
|
|
|
if (response.ok) { |
|
|
const colors = await response.json(); |
|
|
const container = document.getElementById('presetColors'); |
|
|
|
|
|
container.innerHTML = colors.map(color => ` |
|
|
<div class="preset-color" |
|
|
style="background-color: ${color.value}" |
|
|
title="${color.name}" |
|
|
onclick="selectPresetColor('${color.value}')"> |
|
|
</div> |
|
|
`).join(''); |
|
|
} |
|
|
} catch (error) { |
|
|
console.error('Error cargando colores:', error); |
|
|
} |
|
|
} |
|
|
|
|
|
function openClassManager() { |
|
|
document.getElementById('classManagerModal').style.display = 'block'; |
|
|
updateClassList(); |
|
|
} |
|
|
|
|
|
function closeClassManager() { |
|
|
document.getElementById('classManagerModal').style.display = 'none'; |
|
|
cancelEdit(); |
|
|
} |
|
|
|
|
|
function selectPresetColor(color) { |
|
|
document.getElementById('classColor').value = color; |
|
|
} |
|
|
|
|
|
async function saveClass() { |
|
|
const name = document.getElementById('className').value.trim(); |
|
|
const color = document.getElementById('classColor').value; |
|
|
|
|
|
if (!name) { |
|
|
showAlert('Por favor ingresa un nombre para la clase', 'error'); |
|
|
return; |
|
|
} |
|
|
|
|
|
try { |
|
|
const sessionName = currentSession; |
|
|
const method = editingClassId ? 'PUT' : 'POST'; |
|
|
const url = editingClassId |
|
|
? `/api/classes/${editingClassId}` |
|
|
: '/api/classes/'; |
|
|
|
|
|
const body = editingClassId |
|
|
? { name, color } |
|
|
: { name, color, session_name: sessionName }; |
|
|
|
|
|
const response = await fetch(url, { |
|
|
method: method, |
|
|
headers: { |
|
|
...getAuthHeaders(), |
|
|
'Content-Type': 'application/json' |
|
|
}, |
|
|
body: JSON.stringify(body) |
|
|
}); |
|
|
|
|
|
if (response.ok) { |
|
|
const result = await response.json(); |
|
|
showAlert( |
|
|
editingClassId ? 'Clase actualizada correctamente' : 'Clase creada correctamente', |
|
|
'success' |
|
|
); |
|
|
|
|
|
|
|
|
document.getElementById('className').value = ''; |
|
|
document.getElementById('classColor').value = '#ff0000'; |
|
|
cancelEdit(); |
|
|
|
|
|
|
|
|
await loadUserClasses(currentSession); |
|
|
} else { |
|
|
const error = await response.json(); |
|
|
showAlert(error.detail || 'Error al guardar la clase', 'error'); |
|
|
} |
|
|
} catch (error) { |
|
|
console.error('Error:', error); |
|
|
showAlert('Error de conexión al guardar la clase', 'error'); |
|
|
} |
|
|
} |
|
|
|
|
|
function editClass(dbId, name, color) { |
|
|
editingClassId = dbId; |
|
|
document.getElementById('className').value = name; |
|
|
document.getElementById('classColor').value = color; |
|
|
document.getElementById('cancelEditBtn').style.display = 'inline-block'; |
|
|
|
|
|
|
|
|
document.getElementById('className').focus(); |
|
|
} |
|
|
|
|
|
function cancelEdit() { |
|
|
editingClassId = null; |
|
|
document.getElementById('className').value = ''; |
|
|
document.getElementById('classColor').value = '#ff0000'; |
|
|
document.getElementById('cancelEditBtn').style.display = 'none'; |
|
|
} |
|
|
|
|
|
async function deleteClass(dbId) { |
|
|
if (!confirm('¿Estás seguro de que quieres eliminar esta clase?')) { |
|
|
return; |
|
|
} |
|
|
|
|
|
try { |
|
|
const response = await fetch(`/api/classes/${dbId}`, { |
|
|
method: 'DELETE', |
|
|
headers: getAuthHeaders() |
|
|
}); |
|
|
|
|
|
if (response.ok) { |
|
|
showAlert('Clase eliminada correctamente', 'success'); |
|
|
await loadUserClasses(currentSession); |
|
|
} else { |
|
|
const error = await response.json(); |
|
|
showAlert(error.detail || 'Error al eliminar la clase', 'error'); |
|
|
} |
|
|
} catch (error) { |
|
|
console.error('Error:', error); |
|
|
showAlert('Error de conexión al eliminar la clase', 'error'); |
|
|
} |
|
|
} |
|
|
|
|
|
async function resetToDefaultClasses() { |
|
|
if (!confirm('¿Estás seguro? Esto eliminará todas las clases personalizadas y creará las clases por defecto.')) { |
|
|
return; |
|
|
} |
|
|
|
|
|
try { |
|
|
const sessionName = currentSession; |
|
|
const response = await fetch('/api/classes/reset-to-default', { |
|
|
method: 'POST', |
|
|
headers: { |
|
|
...getAuthHeaders(), |
|
|
'Content-Type': 'application/json' |
|
|
}, |
|
|
body: JSON.stringify({ session_name: sessionName }) |
|
|
}); |
|
|
|
|
|
if (response.ok) { |
|
|
const result = await response.json(); |
|
|
showAlert(result.detail, 'success'); |
|
|
await loadUserClasses(currentSession); |
|
|
} else { |
|
|
const error = await response.json(); |
|
|
showAlert(error.detail || 'Error al restablecer clases', 'error'); |
|
|
} |
|
|
} catch (error) { |
|
|
console.error('Error:', error); |
|
|
showAlert('Error de conexión al restablecer clases', 'error'); |
|
|
} |
|
|
} |
|
|
|
|
|
async function importClassesFromAnnotations() { |
|
|
if (!currentSession) { |
|
|
showAlert('Selecciona una sesión primero', 'error'); |
|
|
return; |
|
|
} |
|
|
|
|
|
try { |
|
|
const response = await fetch('/api/classes/import', { |
|
|
method: 'POST', |
|
|
headers: { |
|
|
...getAuthHeaders(), |
|
|
'Content-Type': 'application/json' |
|
|
}, |
|
|
body: JSON.stringify({ session_name: currentSession }) |
|
|
}); |
|
|
|
|
|
if (response.ok) { |
|
|
const result = await response.json(); |
|
|
showAlert(result.detail, 'success'); |
|
|
await loadUserClasses(currentSession); |
|
|
} else { |
|
|
const error = await response.json(); |
|
|
showAlert(error.detail || 'Error al importar clases', 'error'); |
|
|
} |
|
|
} catch (error) { |
|
|
console.error('Error:', error); |
|
|
showAlert('Error de conexión al importar clases', 'error'); |
|
|
} |
|
|
} |
|
|
|
|
|
function updateClassList() { |
|
|
const container = document.getElementById('classList'); |
|
|
|
|
|
if (classes.length === 0) { |
|
|
container.innerHTML = '<p style="text-align: center; color: #666;">No hay clases definidas</p>'; |
|
|
return; |
|
|
} |
|
|
|
|
|
container.innerHTML = classes.map(cls => ` |
|
|
<div class="class-item-manager"> |
|
|
<div class="class-color-preview" style="background-color: ${cls.color}"></div> |
|
|
<div class="class-info"> |
|
|
<strong>${cls.name}</strong><br> |
|
|
<small>ID: ${cls.id} | Color: ${cls.color}</small> |
|
|
</div> |
|
|
<div class="class-actions"> |
|
|
<button class="btn-small btn-edit" onclick="editClass(${cls.dbId}, '${cls.name}', '${cls.color}')">✏️</button> |
|
|
<button class="btn-small btn-delete" onclick="deleteClass(${cls.dbId})">🗑️</button> |
|
|
</div> |
|
|
</div> |
|
|
`).join(''); |
|
|
} |
|
|
|
|
|
|
|
|
function createClassButtons() { |
|
|
const container = document.getElementById('classButtons'); |
|
|
if (!container || classes.length === 0) return; |
|
|
|
|
|
container.innerHTML = ''; |
|
|
|
|
|
classes.forEach(cls => { |
|
|
const button = document.createElement('button'); |
|
|
button.className = 'class-btn'; |
|
|
button.style.backgroundColor = cls.color; |
|
|
button.style.color = getContrastColor(cls.color); |
|
|
button.style.borderColor = cls.color; |
|
|
button.textContent = cls.name; |
|
|
button.onclick = () => selectClass(cls.id); |
|
|
|
|
|
if (cls.id === selectedClass) { |
|
|
button.classList.add('active'); |
|
|
} |
|
|
|
|
|
container.appendChild(button); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
function getContrastColor(hexColor) { |
|
|
|
|
|
const r = parseInt(hexColor.substr(1, 2), 16); |
|
|
const g = parseInt(hexColor.substr(3, 2), 16); |
|
|
const b = parseInt(hexColor.substr(5, 2), 16); |
|
|
|
|
|
|
|
|
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255; |
|
|
|
|
|
return luminance > 0.5 ? '#000000' : '#ffffff'; |
|
|
} |
|
|
|
|
|
|
|
|
window.onclick = function(event) { |
|
|
const modal = document.getElementById('classManagerModal'); |
|
|
if (event.target === modal) { |
|
|
closeClassManager(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
document.addEventListener('keydown', (e) => { |
|
|
|
|
|
const modal = document.getElementById('classManagerModal'); |
|
|
if (modal && modal.style.display === 'block') { |
|
|
if (e.key === 'Escape') { |
|
|
closeClassManager(); |
|
|
return; |
|
|
} |
|
|
|
|
|
return; |
|
|
} |
|
|
|
|
|
if (e.key >= '1' && e.key <= '9') { |
|
|
const classId = parseInt(e.key) - 1; |
|
|
if (classId < classes.length) { |
|
|
selectClass(classId); |
|
|
} |
|
|
} else if (e.key === 'Escape') { |
|
|
isDrawing = false; |
|
|
redrawCanvas(); |
|
|
} else if (e.key === 'Delete') { |
|
|
if (annotations.length > 0) { |
|
|
annotations.pop(); |
|
|
updateAnnotationsList(); |
|
|
redrawCanvas(); |
|
|
} |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', () => { |
|
|
const token = localStorage.getItem('access_token'); |
|
|
|
|
|
if (!token) { |
|
|
window.location.href = '/login'; |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
fetch('/auth/profile', { |
|
|
headers: getAuthHeaders() |
|
|
}) |
|
|
.then(response => { |
|
|
if (!response.ok) { |
|
|
throw new Error('Token inválido'); |
|
|
} |
|
|
return response.json(); |
|
|
}) |
|
|
.then(data => { |
|
|
if (data.success) { |
|
|
|
|
|
updateUserInfo(data.user); |
|
|
|
|
|
loadSessions(); |
|
|
} else { |
|
|
throw new Error('Error de autenticación'); |
|
|
} |
|
|
}) |
|
|
.catch(error => { |
|
|
console.error('Error de autenticación:', error); |
|
|
localStorage.removeItem('access_token'); |
|
|
localStorage.removeItem('token_type'); |
|
|
window.location.href = '/login'; |
|
|
}); |
|
|
|
|
|
|
|
|
const urlParams = new URLSearchParams(window.location.search); |
|
|
const sessionParam = urlParams.get('session'); |
|
|
if (sessionParam) { |
|
|
setTimeout(() => { |
|
|
document.getElementById('sessionSelect').value = sessionParam; |
|
|
}, 1000); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
function updateUserInfo(user) { |
|
|
|
|
|
const userBadge = document.querySelector('.user-badge'); |
|
|
if (userBadge) { |
|
|
userBadge.innerHTML = ` |
|
|
👤 ${user.username} |
|
|
${(user && user.is_admin) ? '<span class="admin-badge">ADMIN</span>' : ''} |
|
|
`; |
|
|
} |
|
|
|
|
|
|
|
|
const userInfo = document.querySelector('.user-info'); |
|
|
if (userInfo && !userInfo.querySelector('.logout-btn')) { |
|
|
const logoutBtn = document.createElement('button'); |
|
|
logoutBtn.className = 'logout-btn'; |
|
|
logoutBtn.textContent = 'Cerrar Sesión'; |
|
|
logoutBtn.onclick = logout; |
|
|
userInfo.appendChild(logoutBtn); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
async function logout() { |
|
|
try { |
|
|
|
|
|
await fetch('/auth/logout', { |
|
|
method: 'POST', |
|
|
credentials: 'include' |
|
|
}); |
|
|
} catch (error) { |
|
|
console.log('Error en logout del servidor:', error); |
|
|
} |
|
|
|
|
|
|
|
|
localStorage.removeItem('token'); |
|
|
localStorage.removeItem('user'); |
|
|
window.location.href = '/login'; |
|
|
} |
|
|
</script> |
|
|
</body> |
|
|
</html> |
|
|
|