|
|
|
|
|
|
|
|
|
|
|
let currentClass = 0; |
|
|
let annotations = []; |
|
|
let annotationCounter = 0; |
|
|
let isSelecting = false; |
|
|
let startX, startY; |
|
|
let currentBox = null; |
|
|
let originalImageData = null; |
|
|
|
|
|
|
|
|
const classes = { |
|
|
0: {"name": "objeto 1", "color": "#ff0000"}, |
|
|
1: {"name": "objeto 2", "color": "#00ff00"}, |
|
|
2: {"name": "objeto 3", "color": "#0000ff"}, |
|
|
3: {"name": "objeto 4", "color": "#ffff00"}, |
|
|
4: {"name": "objeto 5", "color": "#ff00ff"}, |
|
|
5: {"name": "objeto 6", "color": "#00ffff"} |
|
|
}; |
|
|
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', function() { |
|
|
|
|
|
initializeClassButtons(); |
|
|
refreshSessions(); |
|
|
|
|
|
|
|
|
document.getElementById('imageForm').addEventListener('submit', handleImageUpload); |
|
|
document.getElementById('refreshSessions')?.addEventListener('click', refreshSessions); |
|
|
|
|
|
|
|
|
const xSlider = document.getElementById('xPos'); |
|
|
const ySlider = document.getElementById('yPos'); |
|
|
const xValue = document.getElementById('xValue'); |
|
|
const yValue = document.getElementById('yValue'); |
|
|
|
|
|
if (xSlider && xValue) { |
|
|
xSlider.addEventListener('input', function() { |
|
|
xValue.textContent = this.value; |
|
|
}); |
|
|
} |
|
|
|
|
|
if (ySlider && yValue) { |
|
|
ySlider.addEventListener('input', function() { |
|
|
yValue.textContent = this.value; |
|
|
}); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
function initializeClassButtons() { |
|
|
const container = document.getElementById('classButtons'); |
|
|
if (!container) return; |
|
|
|
|
|
container.innerHTML = Object.entries(classes).map(([id, cls]) => ` |
|
|
<button type="button" class="class-btn" data-class="${id}" style="background-color: ${cls.color};"> |
|
|
${cls.name} |
|
|
</button> |
|
|
`).join(''); |
|
|
|
|
|
|
|
|
container.querySelectorAll('.class-btn').forEach(btn => { |
|
|
btn.addEventListener('click', function() { |
|
|
currentClass = parseInt(this.dataset.class); |
|
|
|
|
|
|
|
|
container.querySelectorAll('.class-btn').forEach(b => b.classList.remove('active')); |
|
|
this.classList.add('active'); |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
container.querySelector('.class-btn')?.classList.add('active'); |
|
|
} |
|
|
|
|
|
|
|
|
async function handleImageUpload(event) { |
|
|
event.preventDefault(); |
|
|
|
|
|
const formData = new FormData(event.target); |
|
|
|
|
|
try { |
|
|
const response = await fetch('/generate', { |
|
|
method: 'POST', |
|
|
body: formData |
|
|
}); |
|
|
|
|
|
const result = await response.json(); |
|
|
|
|
|
if (result.image) { |
|
|
displayImage(result.image); |
|
|
clearAnnotations(); |
|
|
} |
|
|
} catch (error) { |
|
|
alert('Error al procesar imagen: ' + error.message); |
|
|
} |
|
|
} |
|
|
|
|
|
function displayImage(base64Image) { |
|
|
const canvas = document.getElementById('imageCanvas'); |
|
|
const ctx = canvas.getContext('2d'); |
|
|
|
|
|
const img = new Image(); |
|
|
img.onload = function() { |
|
|
canvas.width = img.width; |
|
|
canvas.height = img.height; |
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height); |
|
|
ctx.drawImage(img, 0, 0); |
|
|
|
|
|
|
|
|
originalImageData = base64Image; |
|
|
|
|
|
|
|
|
document.getElementById('annotationControls').style.display = 'block'; |
|
|
canvas.style.display = 'block'; |
|
|
|
|
|
|
|
|
setupCanvasEvents(); |
|
|
}; |
|
|
img.src = base64Image; |
|
|
} |
|
|
|
|
|
|
|
|
function setupCanvasEvents() { |
|
|
const canvas = document.getElementById('imageCanvas'); |
|
|
|
|
|
|
|
|
canvas.removeEventListener('mousedown', handleMouseDown); |
|
|
canvas.removeEventListener('mousemove', handleMouseMove); |
|
|
canvas.removeEventListener('mouseup', handleMouseUp); |
|
|
|
|
|
canvas.addEventListener('mousedown', handleMouseDown); |
|
|
canvas.addEventListener('mousemove', handleMouseMove); |
|
|
canvas.addEventListener('mouseup', handleMouseUp); |
|
|
} |
|
|
|
|
|
function handleMouseDown(e) { |
|
|
if (currentClass === undefined) return; |
|
|
|
|
|
const rect = e.target.getBoundingClientRect(); |
|
|
startX = e.clientX - rect.left; |
|
|
startY = e.clientY - rect.top; |
|
|
isSelecting = true; |
|
|
|
|
|
|
|
|
currentBox = document.createElement('div'); |
|
|
currentBox.style.position = 'absolute'; |
|
|
currentBox.style.border = '2px solid ' + classes[currentClass].color; |
|
|
currentBox.style.backgroundColor = classes[currentClass].color + '20'; |
|
|
currentBox.style.pointerEvents = 'none'; |
|
|
currentBox.style.fontSize = '12px'; |
|
|
currentBox.style.color = classes[currentClass].color; |
|
|
currentBox.style.padding = '2px'; |
|
|
currentBox.style.zIndex = '1000'; |
|
|
} |
|
|
|
|
|
function handleMouseMove(e) { |
|
|
if (!isSelecting || !currentBox) return; |
|
|
|
|
|
const rect = e.target.getBoundingClientRect(); |
|
|
const currentX = e.clientX - rect.left; |
|
|
const currentY = e.clientY - rect.top; |
|
|
|
|
|
const left = Math.min(startX, currentX); |
|
|
const top = Math.min(startY, currentY); |
|
|
const width = Math.abs(currentX - startX); |
|
|
const height = Math.abs(currentY - startY); |
|
|
|
|
|
const classColor = classes[currentClass].color; |
|
|
currentBox.style.borderColor = classColor; |
|
|
currentBox.style.backgroundColor = classColor + '20'; |
|
|
currentBox.style.left = (rect.left + left) + 'px'; |
|
|
currentBox.style.top = (rect.top + top + window.scrollY) + 'px'; |
|
|
currentBox.style.width = width + 'px'; |
|
|
currentBox.style.height = height + 'px'; |
|
|
currentBox.textContent = classes[currentClass].name; |
|
|
|
|
|
document.body.appendChild(currentBox); |
|
|
} |
|
|
|
|
|
function handleMouseUp(e) { |
|
|
if (!isSelecting || !currentBox) return; |
|
|
|
|
|
const rect = e.target.getBoundingClientRect(); |
|
|
const endX = e.clientX - rect.left; |
|
|
const endY = e.clientY - rect.top; |
|
|
|
|
|
const left = Math.min(startX, endX); |
|
|
const top = Math.min(startY, endY); |
|
|
const width = Math.abs(endX - startX); |
|
|
const height = Math.abs(endY - startY); |
|
|
|
|
|
if (width > 5 && height > 5) { |
|
|
addAnnotation(left, top, width, height, currentClass); |
|
|
} |
|
|
|
|
|
if (currentBox) { |
|
|
currentBox.remove(); |
|
|
currentBox = null; |
|
|
} |
|
|
|
|
|
isSelecting = false; |
|
|
} |
|
|
|
|
|
|
|
|
function addAnnotation(x, y, width, height, classId) { |
|
|
const annotation = { |
|
|
id: annotationCounter++, |
|
|
x: Math.round(x), |
|
|
y: Math.round(y), |
|
|
width: Math.round(width), |
|
|
height: Math.round(height), |
|
|
class_id: classId, |
|
|
class_name: classes[classId].name |
|
|
}; |
|
|
|
|
|
annotations.push(annotation); |
|
|
updateAnnotationsList(); |
|
|
redrawCanvas(); |
|
|
} |
|
|
|
|
|
function removeAnnotation(id) { |
|
|
annotations = annotations.filter(ann => ann.id !== id); |
|
|
updateAnnotationsList(); |
|
|
redrawCanvas(); |
|
|
} |
|
|
|
|
|
function updateAnnotationsList() { |
|
|
const container = document.getElementById('annotationsList'); |
|
|
if (!container) return; |
|
|
|
|
|
if (annotations.length === 0) { |
|
|
container.innerHTML = '<p>Sin anotaciones aún</p>'; |
|
|
return; |
|
|
} |
|
|
|
|
|
container.innerHTML = annotations.map(ann => ` |
|
|
<div class="annotation-item" style="border-left-color: ${classes[ann.class_id].color}"> |
|
|
<div> |
|
|
<strong>${ann.class_name}</strong><br> |
|
|
<small>x:${ann.x}, y:${ann.y}, w:${ann.width}, h:${ann.height}</small> |
|
|
</div> |
|
|
<button class="delete-annotation" onclick="removeAnnotation(${ann.id})">❌</button> |
|
|
</div> |
|
|
`).join(''); |
|
|
} |
|
|
|
|
|
function redrawCanvas() { |
|
|
const canvas = document.getElementById('imageCanvas'); |
|
|
if (!canvas || !originalImageData) return; |
|
|
|
|
|
const ctx = canvas.getContext('2d'); |
|
|
const img = new Image(); |
|
|
|
|
|
img.onload = function() { |
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height); |
|
|
ctx.drawImage(img, 0, 0); |
|
|
|
|
|
|
|
|
annotations.forEach(ann => { |
|
|
ctx.strokeStyle = classes[ann.class_id].color; |
|
|
ctx.lineWidth = 2; |
|
|
ctx.strokeRect(ann.x, ann.y, ann.width, ann.height); |
|
|
|
|
|
|
|
|
ctx.fillStyle = classes[ann.class_id].color; |
|
|
ctx.fillRect(ann.x, ann.y - 20, ctx.measureText(ann.class_name).width + 10, 20); |
|
|
ctx.fillStyle = 'white'; |
|
|
ctx.font = '14px Arial'; |
|
|
ctx.fillText(ann.class_name, ann.x + 5, ann.y - 5); |
|
|
}); |
|
|
}; |
|
|
img.src = originalImageData; |
|
|
} |
|
|
|
|
|
function clearAnnotations() { |
|
|
annotations = []; |
|
|
annotationCounter = 0; |
|
|
updateAnnotationsList(); |
|
|
if (document.getElementById('imageCanvas').style.display !== 'none') { |
|
|
redrawCanvas(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
async function saveAnnotations() { |
|
|
const filename = document.getElementById('filename').value; |
|
|
const sessionName = document.getElementById('sessionName').value || 'default'; |
|
|
|
|
|
if (!filename) { |
|
|
alert('Por favor ingresa un nombre para el archivo'); |
|
|
return; |
|
|
} |
|
|
|
|
|
if (annotations.length === 0) { |
|
|
alert('No hay anotaciones para guardar'); |
|
|
return; |
|
|
} |
|
|
|
|
|
const canvas = document.getElementById('imageCanvas'); |
|
|
|
|
|
if (!originalImageData) { |
|
|
alert('No hay imagen original para guardar'); |
|
|
return; |
|
|
} |
|
|
|
|
|
const formData = new FormData(); |
|
|
formData.append('annotations', JSON.stringify(annotations)); |
|
|
formData.append('filename', filename); |
|
|
formData.append('session_name', sessionName); |
|
|
formData.append('image_width', canvas.width); |
|
|
formData.append('image_height', canvas.height); |
|
|
formData.append('image_data', originalImageData); |
|
|
|
|
|
try { |
|
|
const response = await fetch('/save_annotations', { |
|
|
method: 'POST', |
|
|
body: formData |
|
|
}); |
|
|
|
|
|
const result = await response.json(); |
|
|
if (result.success) { |
|
|
alert(`✅ Dataset guardado correctamente! |
|
|
|
|
|
📄 Nombre solicitado: "${result.original_name}" |
|
|
🔢 Nombre único generado: "${result.unique_name}" |
|
|
|
|
|
📁 Archivos creados: |
|
|
• Imagen: ${result.files.image} |
|
|
• Etiquetas: ${result.files.labels} |
|
|
|
|
|
📊 Objetos anotados: ${annotations.length} |
|
|
|
|
|
🎯 Formato YOLO guardado: |
|
|
${result.yolo_format.join('\\n')} |
|
|
|
|
|
ℹ️ La imagen se guardó SIN las marcas de selección`); |
|
|
|
|
|
refreshSessions(); |
|
|
} else { |
|
|
alert(`❌ Error: ${result.message}`); |
|
|
} |
|
|
} catch (error) { |
|
|
alert('Error al guardar: ' + error.message); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
async function refreshSessions() { |
|
|
try { |
|
|
const response = await fetch('/api/sessions'); |
|
|
const result = await response.json(); |
|
|
|
|
|
if (result.success) { |
|
|
displaySessions(result.sessions); |
|
|
} else { |
|
|
alert(`Error al cargar sesiones: ${result.message}`); |
|
|
} |
|
|
} catch (error) { |
|
|
alert('Error al cargar sesiones: ' + error.message); |
|
|
} |
|
|
} |
|
|
|
|
|
function displaySessions(sessions) { |
|
|
const sessionsList = document.getElementById('sessionsList'); |
|
|
if (!sessionsList) return; |
|
|
|
|
|
if (sessions.length === 0) { |
|
|
sessionsList.innerHTML = '<p>No hay sesiones disponibles</p>'; |
|
|
return; |
|
|
} |
|
|
|
|
|
sessionsList.innerHTML = sessions.map(session => ` |
|
|
<div class="session-item" onclick="selectSession('${session.name}')"> |
|
|
<div><strong>${session.name}</strong></div> |
|
|
<div class="session-stats"> |
|
|
📷 ${session.images_count} imágenes | 📝 ${session.labels_count} etiquetas |
|
|
</div> |
|
|
<div class="session-actions"> |
|
|
<button class="download-btn" onclick="downloadSession('${session.name}'); event.stopPropagation();" title="Descargar sesión"> |
|
|
💾 Descargar |
|
|
</button> |
|
|
<button class="visualize-btn" onclick="visualizeSession('${session.name}'); event.stopPropagation();" title="Visualizar sesión"> |
|
|
👁️ Visualizar |
|
|
</button> |
|
|
<button class="delete-btn" onclick="deleteSession('${session.name}'); event.stopPropagation();" title="Eliminar sesión"> |
|
|
🗑️ Eliminar |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
`).join(''); |
|
|
} |
|
|
|
|
|
function selectSession(sessionName) { |
|
|
const sessionInput = document.getElementById('sessionName'); |
|
|
if (sessionInput) { |
|
|
sessionInput.value = sessionName; |
|
|
} |
|
|
|
|
|
|
|
|
document.querySelectorAll('.session-item').forEach(item => { |
|
|
item.classList.remove('selected'); |
|
|
}); |
|
|
event.target.closest('.session-item').classList.add('selected'); |
|
|
} |
|
|
|
|
|
async function downloadSession(sessionName) { |
|
|
try { |
|
|
const downloadLink = document.createElement('a'); |
|
|
downloadLink.href = `/download_session/${sessionName}`; |
|
|
downloadLink.download = `dataset_${sessionName}.zip`; |
|
|
|
|
|
document.body.appendChild(downloadLink); |
|
|
downloadLink.click(); |
|
|
document.body.removeChild(downloadLink); |
|
|
|
|
|
alert(`🎉 Descargando dataset '${sessionName}' como archivo ZIP |
|
|
|
|
|
📦 Estructura YOLO estándar: |
|
|
├── images/ (imágenes .jpg) |
|
|
└── labels/ (etiquetas .txt) |
|
|
|
|
|
📁 Archivo: dataset_${sessionName}_YYYYMMDD_HHMMSS.zip |
|
|
🎯 Formato listo para entrenamiento YOLO`); |
|
|
|
|
|
} catch (error) { |
|
|
alert('Error al descargar sesión: ' + error.message); |
|
|
} |
|
|
} |
|
|
|
|
|
function visualizeSession(sessionName) { |
|
|
localStorage.setItem('currentSession', sessionName); |
|
|
window.open(`/visualizer?session=${sessionName}`, '_top'); |
|
|
} |
|
|
|
|
|
async function deleteSession(sessionName) { |
|
|
const confirmation = confirm(`⚠️ ¿Estás seguro de que quieres eliminar la sesión "${sessionName}"? |
|
|
|
|
|
🗑️ Esta acción eliminará permanentemente: |
|
|
• Todas las imágenes de la sesión |
|
|
• Todas las etiquetas/anotaciones |
|
|
• No se puede deshacer |
|
|
|
|
|
¿Continuar con la eliminación?`); |
|
|
|
|
|
if (!confirmation) return; |
|
|
|
|
|
try { |
|
|
const response = await fetch(`/delete_session/${sessionName}`, { |
|
|
method: 'DELETE' |
|
|
}); |
|
|
|
|
|
const result = await response.json(); |
|
|
|
|
|
if (result.success) { |
|
|
alert(`✅ Sesión "${sessionName}" eliminada correctamente`); |
|
|
refreshSessions(); |
|
|
|
|
|
const currentSelected = document.getElementById('sessionName').value; |
|
|
if (currentSelected === sessionName) { |
|
|
document.getElementById('sessionName').value = 'default'; |
|
|
} |
|
|
} else { |
|
|
alert(`❌ Error al eliminar sesión: ${result.message}`); |
|
|
} |
|
|
} catch (error) { |
|
|
alert('❌ Error de conexión al eliminar sesión: ' + error.message); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
window.removeAnnotation = removeAnnotation; |
|
|
window.saveAnnotations = saveAnnotations; |
|
|
window.clearAnnotations = clearAnnotations; |
|
|
window.selectSession = selectSession; |
|
|
window.downloadSession = downloadSession; |
|
|
window.visualizeSession = visualizeSession; |
|
|
window.deleteSession = deleteSession; |
|
|
|