jebin2's picture
deploy issue fix
ca23a7f
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>📸 Annotator</title>
<!-- Xterm.js Files -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.min.css" />
<script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-web-links@0.9.0/lib/xterm-addon-web-links.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #f8fafc;
min-height: 100vh;
color: #1a202c;
}
/* Top Navigation Bar */
.top-nav {
background: white;
border-bottom: 1px solid #e2e8f0;
padding: 16px 24px;
display: flex;
align-items: center;
justify-content: space-between;
position: sticky;
top: 0;
z-index: 100;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.logo {
font-size: 20px;
font-weight: 700;
color: #2d3748;
display: flex;
align-items: center;
gap: 8px;
}
.nav-actions {
display: flex;
gap: 12px;
align-items: center;
}
/* Main Layout */
.main-container {
display: flex;
height: calc(100vh - 72px);
overflow: hidden;
}
/* Left Sidebar */
.sidebar {
width: 320px;
background: white;
border-right: 1px solid #e2e8f0;
display: flex;
flex-direction: column;
overflow-y: auto;
}
.sidebar-section {
padding: 20px;
border-bottom: 1px solid #f1f5f9;
}
.sidebar-section:last-child {
border-bottom: none;
}
.section-title {
font-size: 14px;
font-weight: 600;
color: #4a5568;
margin-bottom: 12px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* Canvas Area */
.canvas-area {
flex: 1;
display: flex;
flex-direction: column;
background: #f8fafc;
position: relative;
}
.canvas-toolbar {
background: white;
border-bottom: 1px solid #e2e8f0;
padding: 12px 20px;
display: flex;
align-items: center;
gap: 16px;
}
.canvas-container {
flex: 1;
overflow: auto;
display: flex;
/* justify-content: center; */
align-items: flex-start;
padding: 20px;
width: fit-content;
}
canvas {
border: 2px solid #e2e8f0;
border-radius: 8px;
cursor: crosshair;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
background: white;
width: 100%;
}
canvas:hover {
border-color: #4299e1;
}
.canvas-placeholder {
text-align: center;
padding: 60px 40px;
color: #a0aec0;
background: white;
border-radius: 12px;
border: 2px dashed #e2e8f0;
}
/* Form Elements */
/* .form-field {
margin-bottom: 16px;
} */
.form-label {
display: block;
font-size: 13px;
font-weight: 500;
color: #4a5568;
margin-bottom: 6px;
}
.form-select,
.form-input {
width: 100%;
padding: 10px 12px;
border: 1px solid #d1d5db;
border-radius: 6px;
font-size: 14px;
background: white;
transition: all 0.2s;
}
.form-select:focus,
.form-input:focus {
outline: none;
border-color: #4299e1;
box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.1);
}
/* Buttons */
.btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
border: none;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
text-decoration: none;
justify-content: center;
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn-primary {
background: #4299e1;
color: white;
}
.btn-primary:hover:not(:disabled) {
background: #3182ce;
}
.btn-secondary {
background: #718096;
color: white;
}
.btn-secondary:hover:not(:disabled) {
background: #4a5568;
}
.btn-success {
background: #48bb78;
color: white;
}
.btn-success:hover:not(:disabled) {
background: #38a169;
}
.btn-danger {
background: #f56565;
color: white;
}
.btn-danger:hover:not(:disabled) {
background: #e53e3e;
}
.btn-ghost {
background: transparent;
color: #4a5568;
border: 1px solid #e2e8f0;
}
.btn-ghost:hover:not(:disabled) {
background: #f7fafc;
border-color: #cbd5e0;
}
.btn-sm {
padding: 6px 12px;
font-size: 12px;
}
.btn-block {
width: 100%;
}
.trainBtn {
width: 100%;
margin-top: 10px;
}
/* Navigation Controls */
.image-nav {
display: flex;
align-items: center;
gap: 12px;
background: #f7fafc;
padding: 12px;
border-radius: 8px;
}
.nav-counter {
font-size: 13px;
font-weight: 500;
color: #4a5568;
min-width: 80px;
text-align: center;
word-break: break-word;
}
/* Progress Indicator */
.progress-section {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
border-radius: 8px;
margin-bottom: 16px;
}
.progress-stat {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
font-size: 14px;
}
.progress-bar {
width: 100%;
height: 6px;
background: rgba(255, 255, 255, 0.3);
border-radius: 3px;
overflow: hidden;
margin-top: 12px;
}
.progress-fill {
height: 100%;
background: #48bb78;
transition: width 0.3s ease;
}
/* Info Cards */
.info-card {
background: #f7fafc;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 16px;
}
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid #e2e8f0;
}
.info-row:last-child {
border-bottom: none;
}
.info-label {
font-size: 13px;
color: #4a5568;
font-weight: 500;
}
.info-value {
font-size: 13px;
color: #1a202c;
font-weight: 600;
}
/* File Upload */
.file-upload {
position: relative;
overflow: hidden;
}
.file-upload input[type=file] {
position: absolute;
opacity: 0;
left: -9999px;
}
.file-upload-label {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 32px 20px;
border: 2px dashed #cbd5e0;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
color: #4a5568;
}
.file-upload-label:hover {
border-color: #4299e1;
background: #f7fafc;
}
/* Alerts */
.alerts {
position: fixed;
top: 80px;
right: 20px;
z-index: 1000;
}
.alert {
padding: 12px 16px;
border-radius: 6px;
margin-bottom: 8px;
font-size: 14px;
font-weight: 500;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
animation: slideIn 0.3s ease;
}
.alert-success {
background: #c6f6d5;
color: #22543d;
border: 1px solid #9ae6b4;
}
.alert-error {
background: #fed7d7;
color: #742a2a;
border: 1px solid #fc8181;
}
.alert-info {
background: #bee3f8;
color: #2a4365;
border: 1px solid #90cdf4;
}
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
/* Quick Help */
.help-section {
background: #fffbf0;
border: 1px solid #fbd38d;
border-radius: 8px;
padding: 16px;
}
.help-item {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
font-size: 12px;
color: #744210;
}
.help-item:last-child {
margin-bottom: 0;
}
.kbd {
background: #2d3748;
color: white;
padding: 2px 6px;
border-radius: 3px;
font-family: monospace;
font-size: 11px;
}
/* Responsive */
@media (max-width: 1024px) {
.sidebar {
width: 280px;
}
}
@media (max-width: 768px) {
.main-container {
flex-direction: column;
}
.sidebar {
width: 100%;
height: auto;
max-height: 300px;
}
.canvas-area {
height: calc(100vh - 372px);
}
}
.form-input-sm {
padding: 4px 6px;
font-size: 11px;
height: auto;
}
.point-edit-row {
display: flex;
align-items: center;
gap: 4px;
margin-bottom: 4px;
padding: 4px;
background: #f8fafc;
border-radius: 4px;
}
.point-edit-input {
flex: 1;
padding: 2px 4px;
font-size: 15px;
border: 1px solid #d1d5db;
border-radius: 3px;
width: 50px;
}
.point-delete-btn {
background: #f56565;
color: white;
border: none;
border-radius: 3px;
padding: 2px 6px;
font-size: 15px;
cursor: pointer;
min-width: 20px;
}
.point-delete-btn:hover {
background: #e53e3e;
}
/* Add to your existing CSS */
.point-highlighted {
background: rgba(255, 215, 0, 0.8) !important;
border: 3px solid #ff6b00 !important;
box-shadow: 0 0 10px rgba(255, 107, 0, 0.6) !important;
}
.point-edit-row.highlighted {
background: #fff3cd !important;
border: 2px solid #ffc107 !important;
border-radius: 6px !important;
box-shadow: 0 2px 8px rgba(255, 193, 7, 0.3) !important;
}
.point-edit-input.highlighted {
border-color: #ffc107 !important;
background: #fff3cd !important;
}
.modal {
display: none;
/* Hidden by default */
position: fixed;
/* Stay in place */
z-index: 1000;
/* Sit on top */
left: 0;
top: 0;
width: 100%;
/* Full width */
height: 100%;
/* Full height */
overflow: auto;
/* Enable scroll if needed */
/* Black w/ opacity */
background-color: rgba(0, 0, 0, 0.8);
}
.modal-content {
background-color: #fefefe;
/* White background */
margin: 2% 0 0 28%;
/* 15% from the top and centered */
padding: 20px;
border: 1px solid #888;
/* Border */
width: 80%;
/* Could be more or less, depending on screen size */
max-width: 600px;
/* Maximum width */
border-radius: 8px;
/* Rounded corners */
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
/* Shadow */
background: linear-gradient(135deg, #fff, #f0f0f0);
border: 4px solid #000000;
border-radius: 20px;
box-shadow: 0 0 30px rgba(0, 0, 0, 0.3);
padding: 30px;
}
#outputModal .modal-content {
height: 85vh;
background: black;
overflow: hidden;
/* Xterm handles its own scrollbar */
padding: 15px;
box-sizing: border-box;
}
.all-button {
position: fixed;
top: 50px;
right: 10%;
display: flex;
flex-direction: column;
gap: 10px;
z-index: 1000;
}
.clear-button,
.stop-button {
float: right;
font-size: 0.8em;
width: auto;
padding: 8px 12px;
margin: 2px;
margin-bottom: 10px;
}
.close {
color: #FF6B6B;
font-size: 40px;
transition: all 0.3s ease;
cursor: pointer;
position: fixed;
top: 22px;
z-index: 10000;
}
</style>
</head>
<body>
<!-- Top Navigation -->
<div class="top-nav">
<div class="logo">
📸 Annotator
</div>
<div class="nav-actions">
<button class="btn btn-primary btn-sm" id="detectBtn" style="display: none;">
🔄 Detect
</button>
<button class="btn btn-secondary btn-sm" id="downloadBtn" style="display: none;">
📥 Download
</button>
<button class="btn btn-primary btn-sm" id="reloadBtn">
🔄 Reload
</button>
<button class="btn btn-ghost btn-sm" id="prevBtn" disabled>
← Prev
</button>
<button class="btn btn-ghost btn-sm" id="nextBtn" disabled>
Next →
</button>
<button class="btn btn-success btn-sm" id="saveBtn">
💾 Save
</button>
<button class="btn btn-secondary btn-sm" id="undoBtn">
↩️ Undo
</button>
<button class="btn btn-danger btn-sm" id="clearBtn">
🗑️ Clear All
</button>
</div>
</div>
<div class="main-container">
<!-- Left Sidebar -->
<div class="sidebar">
<!-- Image Selection -->
<div class="sidebar-section">
<div class="section-title" style="display: none;">Image Selection</div>
<!-- <div class="image-nav"> -->
<!-- <button class="btn btn-ghost btn-sm" id="prevBtn" disabled>
← Prev
</button> -->
<!-- <div class="nav-counter" id="currentImageDisplay">
No image
</div> -->
<!-- <div class="form-field">
<select class="form-select" id="imageSelect">
<option value="">Choose an image...</option>
</select>
</div> -->
<!-- <button class="btn btn-ghost btn-sm" id="nextBtn" disabled>
Next →
</button> -->
<!-- </div> -->
<!-- Annotation Mode -->
<div class="sidebar-section">
<div class="section-title">Annotation Mode</div>
<div class="form-field">
<select class="form-select" id="annotationMode">
<option value="segmentation">Segmentation (YOLO-seg)</option>
<option value="bbox">Bounding Box (YOLO)</option>
</select>
</div>
<div class="form-field" style="display: none;">
<label class="form-label">Class ID</label>
<input type="number" class="form-input" id="classId" value="0" min="0" max="100">
</div>
</div>
<div class="file-upload">
<input type="file" id="uploadFile" accept="image/*">
<label for="uploadFile" class="file-upload-label">
📤 Drop or click to upload
</label>
</div>
</div>
<!-- Progress -->
<!-- <div class="sidebar-section">
<div class="progress-section">
<h3 style="margin-bottom: 12px; font-size: 16px;">Progress</h3>
<div class="progress-stat">
<span>Annotated</span>
<span><span id="annotatedImages">0</span>/<span id="totalImages">0</span></span>
</div>
<div class="progress-bar">
<div class="progress-fill" id="progressFill" style="width: 0%"></div>
</div>
</div>
</div> -->
<!-- Current Image Info -->
<div class="sidebar-section" id="currentImageInfo" style="display: none;">
<div class="section-title">Current Image</div>
<div class="info-card">
<div class="info-row">
<span class="info-label">Annotations</span>
<span class="info-value" id="boxCount">0</span>
</div>
<div class="info-row">
<span class="info-label">Size</span>
<span class="info-value" id="imageSize">-</span>
</div>
<div class="info-row">
<span class="info-label">Selected</span>
<span class="info-value" id="selectedBoxInfo">None</span>
</div>
</div>
<!-- Enhanced Edit Panel for Selected Annotation -->
<div id="annotationEditPanel" style="display: none; margin-top: 16px;">
<div class="section-title">Edit Selected</div>
<div class="info-card">
<div class="form-field" style="margin-bottom: 12px;">
<label class="form-label">Type</label>
<span class="info-value" id="editType">-</span>
</div>
<div class="form-field" style="margin-bottom: 12px;">
<label class="form-label">Class ID</label>
<input type="number" class="form-input form-input-sm" id="editClassId" min="0" max="100"
style="padding: 6px 8px; font-size: 12px;">
</div>
<!-- Bbox-specific controls -->
<div id="bboxEditControls" style="display: none;">
<div class="form-field" style="margin-bottom: 8px;">
<label class="form-label">Position (X, Y)</label>
<div style="display: flex; gap: 8px;">
<input type="number" class="form-input form-input-sm" id="editLeft" min="0"
style="flex: 1; padding: 4px 6px; font-size: 11px;">
<input type="number" class="form-input form-input-sm" id="editTop" min="0"
style="flex: 1; padding: 4px 6px; font-size: 11px;">
</div>
</div>
<div class="form-field" style="margin-bottom: 12px;">
<label class="form-label">Size (W × H)</label>
<div style="display: flex; gap: 8px;">
<input type="number" class="form-input form-input-sm" id="editWidth" min="10"
style="flex: 1; padding: 4px 6px; font-size: 11px;">
<input type="number" class="form-input form-input-sm" id="editHeight" min="10"
style="flex: 1; padding: 4px 6px; font-size: 11px;">
</div>
</div>
</div>
<!-- Polygon-specific controls -->
<div id="polygonEditControls" style="display: none;">
<div class="form-field" style="margin-bottom: 8px;">
<label class="form-label">Points (<span id="pointCount">0</span>)</label>
<div id="pointsList" style="max-height: 400px; overflow-y: auto;
border: 1px solid #e2e8f0; border-radius: 4px; padding: 8px;">
<!-- Points will be populated here -->
</div>
</div>
</div>
<div style="display: flex; gap: 8px; margin-top: 12px;">
<button class="btn btn-success btn-sm" id="applyEditBtn"
style="flex: 1; padding: 6px 8px; font-size: 11px;">
✓ Apply
</button>
<button class="btn btn-secondary btn-sm" id="cancelEditBtn"
style="flex: 1; padding: 6px 8px; font-size: 11px;">
✗ Cancel
</button>
<button class="btn btn-danger btn-sm" id="deleteEditBtn"
style="padding: 6px 8px; font-size: 11px;">
🗑️
</button>
</div>
</div>
</div>
</div>
<!-- Quick Actions -->
<div class="sidebar-section">
<div class="section-title">Actions</div>
<div style="display: flex; gap: 8px;">
<button class="btn btn-primary btn-sm trainBtn" id="trainBtn" style="flex: 1;">
Train
</button>
<button class="btn btn-ghost btn-sm" id="settingsBtn" style="padding: 6px 8px; font-size: 11px;">
⚙️
</button>
</div>
<button class="btn btn-primary btn-sm trainBtn" id="deployModalBtn">
Deploy Model
</button>
<button class="btn btn-primary btn-sm trainBtn" id="resetModalBtn">
Reset Model
</button>
<!-- <button class="btn btn-primary btn-block" id="reloadBtn">
🔄 Reload Annotations
</button>
<button class="btn btn-primary btn-block" id="detectBtn" style="display: none; margin-top: 8px;">
🔄 Detect Annotations
</button>
<button class="btn btn-secondary btn-block" id="downloadBtn" style="display: none; margin-top: 8px;">
📥 Download
</button> -->
</div>
<!-- Quick Help -->
<div class="sidebar-section">
<div class="section-title">Quick Help</div>
<div class="help-section">
<div class="help-item">
<strong>Draw:</strong> Click & drag on image
</div>
<div class="help-item">
<strong>Move:</strong> <span class="kbd">↑↓←→</span> keys
</div>
<div class="help-item">
<strong>Resize:</strong> <span class="kbd">Shift</span> + arrows
</div>
<div class="help-item">
<strong>Delete:</strong> <span class="kbd">Del</span> key
</div>
<div class="help-item">
<strong>Colors:</strong> 🟢 Saved, 🔴 New, 🔵 Selected
</div>
</div>
</div>
</div>
<!-- Canvas Area -->
<div class="canvas-area">
<div class="canvas-toolbar">
<!-- <span id="file_name" style="font-size: 13px; color: #4a5568;">
Click and drag to create annotation annotations • Select annotations to move or resize
</span> -->
<div class="form-field">
<select class="form-select" id="imageSelect">
<option value="">Choose an image...</option>
</select>
</div>
<span id="annotatedImages">0</span>/<span id="totalImages">0</span>
</div>
<div class="canvas-container">
<canvas id="annotationCanvas" style="display: none;"></canvas>
<div id="canvasPlaceholder" class="canvas-placeholder">
<h3 style="margin-bottom: 8px;">Select an image to start annotating</h3>
<p>Choose from the dropdown or upload a new image</p>
</div>
</div>
</div>
</div>
<!-- Alerts Container -->
<div class="alerts" id="alerts"></div>
<!-- Settings Modal -->
<div id="settingsModal" class="modal">
<div class="modal-content">
<span class="close" id="closeSettingsModal">×</span>
<h2>Train Settings</h2>
<div class="form-field">
<label class="form-label">Epoch</label>
<input type="number" class="form-input" id="epoch" value="10" min="1">
</div>
<div class="form-field">
<label class="form-label">Batch Size</label>
<input type="number" class="form-input" id="batch" value="8" min="1">
</div>
<div class="form-field">
<label class="form-label">Image Size</label>
<input type="number" class="form-input" id="imgsz" value="640" min="1">
</div>
<div class="form-field">
<label class="form-label">
<input type="checkbox" id="recreateDataset" checked>
Recreate Dataset
</label>
</div>
<div class="form-field">
<label class="form-label">
<input type="checkbox" id="resumeTrain" checked>
Resume Train
</label>
</div>
<button class="btn btn-primary" id="saveSettingsBtn">Save</button>
</div>
</div>
<!-- Deploy Modal -->
<div id="deployModal" class="modal">
<div class="modal-content">
<span class="close" id="closeDeployModal">×</span>
<h2>Deploy Model</h2>
<div class="form-field">
<label class="form-label">App Name</label>
<input type="text" class="form-input" id="appName" placeholder="Enter a unique app name">
</div>
<button class="btn btn-primary" id="deployBtn">Deploy</button>
</div>
</div>
<div id="outputModal" class="modal">
<div class="modal-content" style="max-width: none; margin: auto;">
<span class="close" id="closeModal">×</span>
<div class="all-button">
<!-- REMOVED: Scroll control button -->
<button class="stop-button" id="stopTrain">Stop</button>
<button class="clear-button" id="clearOutput">Clear</button>
</div>
<!-- This div will host the xterm.js terminal -->
<div id="output"></div>
</div>
</div>
<script>
// NEW: Xterm.js variables
let term;
let fitAddon;
function getWebSocketURL() {
const isLocal = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
if (isLocal) {
return 'ws://localhost:' + window.location.port + '/ws';
} else {
// Use current domain for Spaces
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
return `${protocol}//${window.location.host}/ws`;
}
}
const socket = new WebSocket(getWebSocketURL());
socket.onmessage = function (event) {
if (!term) return;
const data = JSON.parse(event.data);
term.write(data.data + "\n");
};
class ComicAnnotator {
constructor() {
this.canvas = document.getElementById('annotationCanvas');
this.ctx = this.canvas.getContext('2d');
this.annotations = [];
this.images = [];
this.currentImageIndex = -1;
this.currentImage = null;
this.backgroundImage = null;
this.originalWidth = 0;
this.originalHeight = 0;
// Drawing state
this.isDrawing = false;
this.startX = 0;
this.startY = 0;
this.currentBox = null;
// Segmentation state
this.annotationMode = 'segmentation'; // 'bbox' or 'segmentation'
this.currentPolygon = null;
this.isDrawingPolygon = false;
this.polygonPoints = [];
// Box editing state
this.selectedBoxIndex = -1;
this.isDragging = false;
this.isResizing = false;
this.dragStartX = 0;
this.dragStartY = 0;
this.resizeHandle = null;
this.lastMouseX = 0;
this.lastMouseY = 0;
// Add these new properties for edge resizing
this.isDraggingEdge = false;
this.draggingEdgeIndex = -1;
this.edgeResizeTolerance = 8;
this.showEdgeHandles = true;
// Dynamic annotation detection
this.waitingForDrag = false;
this.initialClickPos = null;
this.clickStartTime = 0;
this.modeIndicatorTimeout = null;
// Add this new property for point highlighting
this.highlightedPointIndex = -1;
this.init();
}
init() {
this.setupEventListeners();
this.loadImages();
this.loadTrainConfig();
}
setupEventListeners() {
// Mode selection
document.getElementById('annotationMode').addEventListener('change', (e) => {
this.annotationMode = e.target.value;
this.selectedBoxIndex = -1;
this.polygonPoints = [];
this.isDrawingPolygon = false;
this.updateCanvasCursor();
this.drawCanvas();
});
// Image selection
document.getElementById('imageSelect').addEventListener('change', (e) => {
if (e.target.value) {
const index = this.images.findIndex(img => img.name === e.target.value);
if (index >= 0) {
this.currentImageIndex = index;
this.loadImage(e.target.value);
}
}
});
// Navigation buttons
document.getElementById('prevBtn').addEventListener('click', () => this.navigatePrevious());
document.getElementById('nextBtn').addEventListener('click', () => this.navigateNext());
// File upload
document.getElementById('uploadFile').addEventListener('change', (e) => {
if (e.target.files[0]) {
this.uploadImage(e.target.files[0]);
}
});
// Action buttons
document.getElementById('saveBtn').addEventListener('click', () => this.saveAnnotations());
document.getElementById('undoBtn').addEventListener('click', () => this.undoLastBox());
document.getElementById('clearBtn').addEventListener('click', () => this.clearAllAnnotations());
document.getElementById('reloadBtn').addEventListener('click', () => this.reloadAnnotations());
document.getElementById('detectBtn').addEventListener('click', () => this.detectAnnotations());
document.getElementById('downloadBtn').addEventListener('click', () => this.downloadAnnotations());
// Canvas events
this.canvas.addEventListener('mousedown', (e) => this.onMouseDown(e));
this.canvas.addEventListener('mousemove', (e) => this.onMouseMove(e));
this.canvas.addEventListener('mouseup', () => this.onMouseUp());
this.canvas.addEventListener('mouseleave', () => this.onMouseUp());
this.canvas.addEventListener('dblclick', (e) => this.onDoubleClick(e));
// Keyboard events
document.addEventListener('keydown', (e) => this.onKeyDown(e));
// Edit panel buttons
document.getElementById('applyEditBtn').addEventListener('click', () => this.applyEdits());
document.getElementById('cancelEditBtn').addEventListener('click', () => this.cancelEdits());
document.getElementById('deleteEditBtn').addEventListener('click', () => this.deleteSelectedBox());
// Real-time bbox updates
document.getElementById('editLeft').addEventListener('input', () => this.updateBboxFromInputs());
document.getElementById('editTop').addEventListener('input', () => this.updateBboxFromInputs());
document.getElementById('editWidth').addEventListener('input', () => this.updateBboxFromInputs());
document.getElementById('editHeight').addEventListener('input', () => this.updateBboxFromInputs());
// Class ID real-time update
document.getElementById('editClassId').addEventListener('input', (e) => {
if (this.selectedBoxIndex >= 0) {
this.annotations[this.selectedBoxIndex].classId = parseInt(e.target.value) || 0;
this.annotations[this.selectedBoxIndex].saved = false;
}
});
// Make canvas focusable for keyboard events
this.canvas.tabIndex = 0;
document.getElementById('trainBtn').addEventListener('click', async (e) => {
try {
this.openXterm();
const response = await fetch('/api/annotate/train');
if (!response.ok) {
throw new Error(`Server error: ${response.status}`);
}
const result = await response.json();
this.showAlert(result.message, 'success');
} catch (error) {
if (term) {
term.write(`\x1b[31m[Error starting command: ${error.message}]\x1b[0m\r\n`);
} else {
this.showAlert('Error starting command: ' + error.message, 'error');
}
}
// Reset file input
document.getElementById('uploadFile').value = '';
});
document.getElementById('stopTrain').addEventListener('click', async (e) => {
this.stopTrain()
});
document.getElementById('clearOutput').addEventListener('click', async (e) => {
this.clearOutput()
});
document.getElementById('closeModal').addEventListener('click', async (e) => {
this.closeTrainModal()
});
// NEW: Add resize listener to refit terminal on window resize
window.addEventListener('resize', () => {
if (document.getElementById('outputModal').style.display === 'block' && fitAddon) {
try {
fitAddon.fit();
} catch (e) {
console.error("Error fitting terminal on resize:", e);
}
}
});
// Settings Modal
document.getElementById('settingsBtn').addEventListener('click', () => {
document.getElementById('settingsModal').style.display = 'block';
});
document.getElementById('closeSettingsModal').addEventListener('click', () => {
document.getElementById('settingsModal').style.display = 'none';
});
document.getElementById('saveSettingsBtn').addEventListener('click', async () => {
const newSettings = {
epoch: parseInt(document.getElementById('epoch').value) || 200,
batch: parseInt(document.getElementById('batch').value) || 10,
imgsz: parseInt(document.getElementById('imgsz').value) || 640,
recreate_dataset: document.getElementById('recreateDataset').checked,
resume_train: document.getElementById('resumeTrain').checked
};
try {
const response = await fetch('/api/annotate/train/config', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(newSettings)
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || 'Failed to save settings');
}
const result = await response.json();
this.trainSettings = newSettings;
document.getElementById('settingsModal').style.display = 'none';
this.showAlert(result.message || 'Settings saved successfully', 'success');
} catch (error) {
console.error('Error saving settings:', error);
this.showAlert('Error: ' + error.message, 'error');
}
});
// Deploy Modal
document.getElementById('deployModalBtn').addEventListener('click', () => {
document.getElementById('deployModal').style.display = 'block';
});
document.getElementById('closeDeployModal').addEventListener('click', () => {
document.getElementById('deployModal').style.display = 'none';
});
document.getElementById('deployBtn').addEventListener('click', async () => {
var appName = document.getElementById('appName').value;
if (!appName) {
appName = "Comic Panel Extractor"
this.showAlert('Using App Name: "Comic Panel Extractor"');
}
try {
const response = await fetch(`/api/annotate/deploy?app_name=${appName}`);
if (!response.ok) {
throw new Error(`Server error: ${response.status}`);
}
const result = await response.json();
this.showAlert(result.message, 'success');
} catch (error) {
if (term) {
term.write(`\x1b[31m[Error starting command: ${error.message}]\x1b[0m\r\n`);
} else {
this.showAlert('Error starting command: ' + error.message, 'error');
}
}
document.getElementById('deployModal').style.display = 'none';
});
// Reset Model
document.getElementById('resetModalBtn').addEventListener('click', async () => {
if (confirm('Are you sure you want to reset the model? This action cannot be undone.')) {
try {
const response = await fetch('/api/annotate/model_reset', { method: 'POST' });
if (!response.ok) {
throw new Error(`Server error: ${response.status}`);
}
const result = await response.json();
this.showAlert(result.message, 'success');
} catch (error) {
if (term) {
term.write(`\x1b[31m[Error starting command: ${error.message}]\x1b[0m\r\n`);
} else {
this.showAlert('Error starting command: ' + error.message, 'error');
}
}
}
});
}
async loadTrainConfig() {
try {
const response = await fetch('/api/annotate/train/config');
const config = await response.json();
this.trainSettings = config;
document.getElementById('epoch').value = config.epoch;
document.getElementById('batch').value = config.batch;
document.getElementById('imgsz').value = config.imgsz;
document.getElementById('recreateDataset').checked = config.recreate_dataset;
document.getElementById('resumeTrain').checked = config.resume_train;
} catch (error) {
this.showAlert('Error loading training config: ' + error.message, 'error');
}
}
updateCanvasCursor() {
if (this.annotationMode === 'segmentation') {
this.canvas.style.cursor = 'crosshair';
} else {
this.canvas.style.cursor = 'crosshair';
}
}
async loadImages() {
try {
const response = await fetch('/api/annotate/images');
this.images = await response.json();
const select = document.getElementById('imageSelect');
select.innerHTML = '<option value="">Choose an image...</option>';
this.images.forEach(img => {
const option = document.createElement('option');
option.value = img.name;
option.textContent = `${img.name} ${img.has_annotations ? '✓' : ''}`;
select.appendChild(option);
});
// Update progress
const annotated = this.images.filter(img => img.has_annotations).length;
document.getElementById('totalImages').textContent = this.images.length;
document.getElementById('annotatedImages').textContent = annotated;
const progress = this.images.length > 0 ? (annotated / this.images.length) * 100 : 0;
// document.getElementById('progressFill').style.width = progress + '%';
this.updateNavigationButtons();
} catch (error) {
this.showAlert('Error loading images: ' + error.message, 'error');
}
}
updateNavigationButtons() {
const prevBtn = document.getElementById('prevBtn');
const nextBtn = document.getElementById('nextBtn');
prevBtn.disabled = this.currentImageIndex <= 0;
nextBtn.disabled = this.currentImageIndex >= this.images.length - 1;
// if (this.currentImageIndex >= 0) {
// const currentImageName = this.images[this.currentImageIndex].name;
// document.getElementById('currentImageDisplay').textContent = `${this.currentImageIndex + 1}/${this.images.length}: ${currentImageName}`;
// } else {
// document.getElementById('currentImageDisplay').textContent = 'No image selected';
// }
}
navigatePrevious() {
if (this.currentImageIndex > 0) {
this.currentImageIndex--;
const imageName = this.images[this.currentImageIndex].name;
document.getElementById('imageSelect').value = imageName;
// document.getElementById('file_name').innerText = imageName;
this.loadImage(imageName);
}
}
navigateNext() {
if (this.currentImageIndex < this.images.length - 1) {
this.currentImageIndex++;
const imageName = this.images[this.currentImageIndex].name;
document.getElementById('imageSelect').value = imageName;
// document.getElementById('file_name').innerText = imageName;
this.loadImage(imageName);
}
}
resetAnnotationStates() {
// Reset selection
this.selectedBoxIndex = -1;
// Reset drawing states
this.isDrawing = false;
this.isDragging = false;
this.isResizing = false;
this.isDraggingEdge = false;
this.isDraggingPoint = false;
// Reset segmentation states
this.isDrawingPolygon = false;
this.polygonPoints = [];
this.currentPolygon = null;
// Reset bbox states
this.currentBox = null;
this.resizeHandle = null;
// Reset drag states
this.draggingEdgeIndex = -1;
this.draggingPointIndex = -1;
this.dragStartX = 0;
this.dragStartY = 0;
this.lastMouseX = 0;
this.lastMouseY = 0;
// Reset dynamic detection states
this.waitingForDrag = false;
this.initialClickPos = null;
this.clickStartTime = 0;
this.highlightedPointIndex = -1;
// Reset cursor
this.canvas.style.cursor = 'crosshair';
// Hide edit panel
const editPanel = document.getElementById('annotationEditPanel');
if (editPanel) {
editPanel.style.display = 'none';
}
}
async loadImage(imageName) {
try {
this.resetAnnotationStates();
// Load image data
const imageResponse = await fetch(`/api/annotate/image/${encodeURIComponent(imageName)}`);
const imageData = await imageResponse.json();
// Load annotations
const annotationsResponse = await fetch(`/api/annotate/annotations/${encodeURIComponent(imageName)}`);
const annotationsData = await annotationsResponse.json();
this.currentImage = imageName;
this.originalWidth = imageData.width;
this.originalHeight = imageData.height;
this.annotations = annotationsData.annotations || [];
this.selectedBoxIndex = -1;
// Load and draw image
const img = new Image();
img.onload = () => {
this.backgroundImage = img;
// Set canvas size to match image
this.canvas.width = img.width;
this.canvas.height = img.height;
this.drawCanvas();
document.getElementById('canvasPlaceholder').style.display = 'none';
this.canvas.style.display = 'block';
document.getElementById('downloadBtn').style.display = 'block';
document.getElementById('detectBtn').style.display = 'block';
};
img.src = imageData.image_data;
// Update info panel
document.getElementById('currentImageInfo').style.display = 'block';
document.getElementById('boxCount').textContent = this.annotations.length;
document.getElementById('imageSize').textContent = `${imageData.width}×${imageData.height}`;
document.getElementById('selectedBoxInfo').textContent = 'None';
this.updateNavigationButtons();
} catch (error) {
this.showAlert('Error loading image: ' + error.message, 'error');
}
}
drawCanvas() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
if (this.backgroundImage) {
this.ctx.drawImage(this.backgroundImage, 0, 0);
}
// Draw existing annotations
this.annotations.forEach((annotation, index) => {
let strokeColor = '#00ff00';
let fillColor = 'rgba(0, 255, 0, 0.2)';
if (index === this.selectedBoxIndex) {
strokeColor = '#0066ff';
fillColor = 'rgba(0, 102, 255, 0.3)';
this.setAnnotationMode(annotation.type);
} else if (!annotation.saved) {
strokeColor = '#ff0000';
fillColor = 'rgba(255, 0, 0, 0.2)';
}
if (annotation.type === 'segmentation') {
this.drawPolygon(annotation.points, strokeColor, fillColor, index === this.selectedBoxIndex);
} else {
this.drawBox(annotation.left, annotation.top, annotation.width, annotation.height, strokeColor, fillColor);
}
// Draw resize handles for selected bbox
if (index === this.selectedBoxIndex && annotation.type === 'bbox') {
this.drawResizeHandles(annotation);
}
});
// Draw current box being drawn
if (this.currentBox) {
this.drawBox(this.currentBox.left, this.currentBox.top, this.currentBox.width, this.currentBox.height, '#ff0000', 'rgba(255, 0, 0, 0.3)');
}
// Draw current polygon being drawn
if (this.currentPolygon && this.currentPolygon.points.length > 0) {
this.drawPolygon(this.currentPolygon.points, '#ff0000', 'rgba(255, 0, 0, 0.3)');
}
}
drawPolygon(points, strokeColor, fillColor, isSelected = false) {
if (points.length < 2) return;
this.ctx.strokeStyle = strokeColor;
this.ctx.fillStyle = fillColor;
this.ctx.lineWidth = 3;
this.ctx.beginPath();
this.ctx.moveTo(points[0].x, points[0].y);
for (let i = 1; i < points.length; i++) {
this.ctx.lineTo(points[i].x, points[i].y);
}
if (points.length > 2) {
this.ctx.closePath();
this.ctx.fill();
}
this.ctx.stroke();
// Draw corner points as squares
const handleSize = isSelected ? 10 : 6;
points.forEach((point, index) => {
// Check if this point should be highlighted
const isHighlighted = isSelected && this.highlightedPointIndex === index;
if (isHighlighted) {
// Draw highlighted point with special styling
this.ctx.fillStyle = '#FFD700'; // Gold color
this.ctx.strokeStyle = '#FF6B00'; // Orange border
this.ctx.lineWidth = 3;
// Draw larger, more visible handle
const highlightSize = handleSize + 6;
this.ctx.fillRect(
point.x - highlightSize / 2,
point.y - highlightSize / 2,
highlightSize,
highlightSize
);
this.ctx.strokeRect(
point.x - highlightSize / 2,
point.y - highlightSize / 2,
highlightSize,
highlightSize
);
// Add pulsing glow effect
this.ctx.shadowColor = '#FF6B00';
this.ctx.shadowBlur = 10;
this.ctx.strokeRect(
point.x - highlightSize / 2,
point.y - highlightSize / 2,
highlightSize,
highlightSize
);
this.ctx.shadowBlur = 0; // Reset shadow
} else {
// Draw normal point
this.ctx.fillStyle = isSelected ? '#0066ff' : strokeColor;
this.ctx.strokeStyle = '#ffffff';
this.ctx.lineWidth = 2;
this.ctx.fillRect(
point.x - handleSize / 2,
point.y - handleSize / 2,
handleSize,
handleSize
);
if (isSelected) {
this.ctx.strokeRect(
point.x - handleSize / 2,
point.y - handleSize / 2,
handleSize,
handleSize
);
}
}
});
// Draw edge handles for selected polygon (existing code)
if (isSelected && points.length > 2 && this.showEdgeHandles) {
this.drawEdgeHandles(points);
}
}
drawEdgeHandles(points) {
this.ctx.fillStyle = 'rgba(0, 102, 255, 0.8)';
this.ctx.strokeStyle = '#ffffff';
this.ctx.lineWidth = 2;
for (let i = 0; i < points.length; i++) {
const nextIndex = (i + 1) % points.length;
const midX = (points[i].x + points[nextIndex].x) / 2;
const midY = (points[i].y + points[nextIndex].y) / 2;
// Draw square handle
const handleSize = 10;
this.ctx.fillRect(midX - handleSize / 2, midY - handleSize / 2, handleSize, handleSize);
this.ctx.strokeRect(midX - handleSize / 2, midY - handleSize / 2, handleSize, handleSize);
}
}
// NEW METHOD: Get edge handle at position
getEdgeHandleAtPosition(x, y, tolerance = 8) {
if (this.selectedBoxIndex >= 0 && this.annotations[this.selectedBoxIndex].type === 'segmentation') {
const points = this.annotations[this.selectedBoxIndex].points;
for (let i = 0; i < points.length; i++) {
const nextIndex = (i + 1) % points.length;
const midX = (points[i].x + points[nextIndex].x) / 2;
const midY = (points[i].y + points[nextIndex].y) / 2;
const distance = Math.sqrt(Math.pow(x - midX, 2) + Math.pow(y - midY, 2));
if (distance <= tolerance) {
return i;
}
}
}
return -1;
}
// NEW METHOD: Resize polygon edge
resizePolygonEdge(annotationIndex, edgeIndex, mouseX, mouseY) {
if (annotationIndex < 0 || annotationIndex >= this.annotations.length) return;
const annotation = this.annotations[annotationIndex];
if (annotation.type !== 'segmentation') return;
const points = annotation.points;
const nextIndex = (edgeIndex + 1) % points.length;
// Get the edge vector
const edgeVectorX = points[nextIndex].x - points[edgeIndex].x;
const edgeVectorY = points[nextIndex].y - points[edgeIndex].y;
// Get the edge normal (perpendicular vector)
const edgeLength = Math.sqrt(edgeVectorX * edgeVectorX + edgeVectorY * edgeVectorY);
if (edgeLength === 0) return;
const normalX = -edgeVectorY / edgeLength;
const normalY = edgeVectorX / edgeLength;
// Get edge midpoint
const edgeMidX = (points[edgeIndex].x + points[nextIndex].x) / 2;
const edgeMidY = (points[edgeIndex].y + points[nextIndex].y) / 2;
// Calculate how far to move the edge
const mouseVectorX = mouseX - edgeMidX;
const mouseVectorY = mouseY - edgeMidY;
// Project mouse movement onto the normal
const moveDistance = mouseVectorX * normalX + mouseVectorY * normalY;
// Move both edge points
const newX1 = Math.max(0, Math.min(this.originalWidth, points[edgeIndex].x + normalX * moveDistance));
const newY1 = Math.max(0, Math.min(this.originalHeight, points[edgeIndex].y + normalY * moveDistance));
const newX2 = Math.max(0, Math.min(this.originalWidth, points[nextIndex].x + normalX * moveDistance));
const newY2 = Math.max(0, Math.min(this.originalHeight, points[nextIndex].y + normalY * moveDistance));
points[edgeIndex].x = newX1;
points[edgeIndex].y = newY1;
points[nextIndex].x = newX2;
points[nextIndex].y = newY2;
annotation.saved = false;
}
// Polygon-specific editing methods
getPolygonAtPosition(x, y) {
for (let i = this.annotations.length - 1; i >= 0; i--) {
const annotation = this.annotations[i];
if (annotation.type === 'segmentation' && this.isPointInPolygon(x, y, annotation.points)) {
return i;
}
}
return -1;
}
isPointInPolygon(x, y, points) {
let inside = false;
for (let i = 0, j = points.length - 1; i < points.length; j = i++) {
if (((points[i].y > y) !== (points[j].y > y)) &&
(x < (points[j].x - points[i].x) * (y - points[i].y) / (points[j].y - points[i].y) + points[i].x)) {
inside = !inside;
}
}
return inside;
}
getPolygonPointAtPosition(x, y, tolerance = 8) {
if (this.selectedBoxIndex >= 0 && this.annotations[this.selectedBoxIndex].type === 'segmentation') {
const points = this.annotations[this.selectedBoxIndex].points;
for (let i = 0; i < points.length; i++) {
const distance = Math.sqrt(Math.pow(x - points[i].x, 2) + Math.pow(y - points[i].y, 2));
if (distance <= tolerance) {
return i;
}
}
}
return -1;
}
getPolygonEdgeAtPosition(x, y, tolerance = 5) {
if (this.selectedBoxIndex >= 0 && this.annotations[this.selectedBoxIndex].type === 'segmentation') {
const points = this.annotations[this.selectedBoxIndex].points;
for (let i = 0; i < points.length; i++) {
const nextIndex = (i + 1) % points.length;
const p1 = points[i];
const p2 = points[nextIndex];
const distance = this.distanceToLineSegment(x, y, p1.x, p1.y, p2.x, p2.y);
if (distance <= tolerance) {
return i; // Return the index of the first point of the edge
}
}
}
return -1;
}
distanceToLineSegment(px, py, x1, y1, x2, y2) {
const dx = x2 - x1;
const dy = y2 - y1;
const length = Math.sqrt(dx * dx + dy * dy);
if (length === 0) return Math.sqrt((px - x1) ** 2 + (py - y1) ** 2);
const t = Math.max(0, Math.min(1, ((px - x1) * dx + (py - y1) * dy) / (length * length)));
const projectionX = x1 + t * dx;
const projectionY = y1 + t * dy;
return Math.sqrt((px - projectionX) ** 2 + (py - projectionY) ** 2);
}
movePolygon(index, deltaX, deltaY) {
if (index >= 0 && index < this.annotations.length && this.annotations[index].type === 'segmentation') {
const annotation = this.annotations[index];
annotation.points = annotation.points.map(point => ({
x: Math.max(0, Math.min(this.originalWidth, point.x + deltaX)),
y: Math.max(0, Math.min(this.originalHeight, point.y + deltaY))
}));
annotation.saved = false;
}
}
movePolygonPoint(annotationIndex, pointIndex, newX, newY) {
if (annotationIndex >= 0 && annotationIndex < this.annotations.length) {
const annotation = this.annotations[annotationIndex];
if (annotation.type === 'segmentation' && pointIndex >= 0 && pointIndex < annotation.points.length) {
annotation.points[pointIndex].x = Math.max(0, Math.min(this.originalWidth, newX));
annotation.points[pointIndex].y = Math.max(0, Math.min(this.originalHeight, newY));
annotation.saved = false;
}
}
}
addPolygonPoint(annotationIndex, edgeIndex, x, y) {
if (annotationIndex >= 0 && annotationIndex < this.annotations.length) {
const annotation = this.annotations[annotationIndex];
if (annotation.type === 'segmentation') {
const insertIndex = edgeIndex + 1;
annotation.points.splice(insertIndex, 0, { x, y });
annotation.saved = false;
}
}
}
removePolygonPoint(annotationIndex, pointIndex) {
if (annotationIndex >= 0 && annotationIndex < this.annotations.length) {
const annotation = this.annotations[annotationIndex];
if (annotation.type === 'segmentation' && annotation.points.length > 3) {
annotation.points.splice(pointIndex, 1);
annotation.saved = false;
}
}
}
drawBox(x, y, width, height, strokeColor, fillColor) {
this.ctx.strokeStyle = strokeColor;
this.ctx.fillStyle = fillColor;
this.ctx.lineWidth = 3;
this.ctx.fillRect(x, y, width, height);
this.ctx.strokeRect(x, y, width, height);
}
drawResizeHandles(box) {
const handleSize = 10;
const handles = [
{ x: box.left - handleSize / 2, y: box.top - handleSize / 2, cursor: 'nw-resize' },
{ x: box.left + box.width - handleSize / 2, y: box.top - handleSize / 2, cursor: 'ne-resize' },
{ x: box.left - handleSize / 2, y: box.top + box.height - handleSize / 2, cursor: 'sw-resize' },
{ x: box.left + box.width - handleSize / 2, y: box.top + box.height - handleSize / 2, cursor: 'se-resize' },
{ x: box.left + box.width / 2 - handleSize / 2, y: box.top - handleSize / 2, cursor: 'n-resize' },
{ x: box.left + box.width / 2 - handleSize / 2, y: box.top + box.height - handleSize / 2, cursor: 's-resize' },
{ x: box.left - handleSize / 2, y: box.top + box.height / 2 - handleSize / 2, cursor: 'w-resize' },
{ x: box.left + box.width - handleSize / 2, y: box.top + box.height / 2 - handleSize / 2, cursor: 'e-resize' }
];
this.ctx.fillStyle = '#0066ff';
this.ctx.strokeStyle = '#ffffff';
this.ctx.lineWidth = 4;
handles.forEach(handle => {
this.ctx.fillRect(handle.x, handle.y, handleSize, handleSize);
this.ctx.strokeRect(handle.x, handle.y, handleSize, handleSize);
});
}
onMouseDown(e) {
if (!this.currentImage) return;
const pos = this.getMousePos(e);
this.lastMouseX = pos.x;
this.lastMouseY = pos.y;
const handle = this.getResizeHandle(pos.x, pos.y);
// Check for existing annotation interactions first (both types)
if (this.selectedBoxIndex >= 0) {
const annotation = this.annotations[this.selectedBoxIndex];
if (annotation.type === 'segmentation') {
// Handle segmentation interactions
const edgeHandleIndex = this.getEdgeHandleAtPosition(pos.x, pos.y);
if (edgeHandleIndex >= 0) {
this.isDraggingEdge = true;
this.draggingEdgeIndex = edgeHandleIndex;
this.canvas.style.cursor = handle.cursor;
return;
}
const pointIndex = this.getPolygonPointAtPosition(pos.x, pos.y);
if (pointIndex >= 0) {
this.isDraggingPoint = true;
this.draggingPointIndex = pointIndex;
this.canvas.style.cursor = handle.cursor;
return;
}
const edgeIndex = this.getPolygonEdgeAtPosition(pos.x, pos.y);
if (edgeIndex >= 0 && e.ctrlKey) {
this.addPolygonPoint(this.selectedBoxIndex, edgeIndex, pos.x, pos.y);
this.drawCanvas();
return;
}
} else if (annotation.type === 'bbox') {
// If clicking on a resize handle
if (handle && handle.type !== 'default' && handle.type !== 'interior') {
this.isResizing = true;
this.resizeHandle = handle;
this.canvas.style.cursor = handle.cursor;
return;
}
// If clicking inside bbox (for moving)
if (handle && handle.type === 'interior') {
this.isDragging = true;
this.dragStartX = pos.x;
this.dragStartY = pos.y;
this.canvas.style.cursor = 'move';
this.updateSelectedBoxInfo();
return;
}
}
}
// IMPORTANT: Check if already drawing a polygon - add point to current polygon
if (this.annotationMode === 'segmentation' && this.isDrawingPolygon) {
this.polygonPoints.push(pos);
this.currentPolygon.points = [...this.polygonPoints];
this.drawCanvas();
return;
}
// Check for clicking on existing annotations
const clickedPolygonIndex = this.getPolygonAtPosition(pos.x, pos.y);
const clickedBoxIndex = this.getBoxAtPosition(pos.x, pos.y);
if (clickedPolygonIndex >= 0) {
this.selectedBoxIndex = clickedPolygonIndex;
this.isDragging = true;
this.dragStartX = pos.x;
this.dragStartY = pos.y;
this.canvas.style.cursor = handle.cursor;
this.updateSelectedBoxInfo();
this.drawCanvas();
return;
}
if (clickedBoxIndex >= 0) {
this.selectedBoxIndex = clickedBoxIndex;
this.isDragging = true;
this.dragStartX = pos.x;
this.dragStartY = pos.y;
this.canvas.style.cursor = handle.cursor;
this.updateSelectedBoxInfo();
this.drawCanvas();
return;
}
// No existing annotation clicked - determine new annotation type
this.selectedBoxIndex = -1;
this.updateSelectedBoxInfo();
// DYNAMIC ANNOTATION TYPE DETECTION (only when not already drawing)
if (e.ctrlKey) {
// Ctrl+Click always starts segmentation
this.startSegmentation(pos);
} else if (e.shiftKey) {
// Shift+Click always starts bbox
this.startBboxDrawing(pos);
} else {
// Default behavior: start with a click and wait for drag
this.waitingForDrag = true;
this.initialClickPos = pos;
this.clickStartTime = Date.now();
}
}
startSegmentation(pos) {
this.setAnnotationMode('segmentation');
this.isDrawingPolygon = true;
this.polygonPoints = [pos];
this.currentPolygon = {
points: [...this.polygonPoints],
classId: parseInt(document.getElementById('classId').value) || 0,
saved: false
};
this.drawCanvas();
this.showModeIndicator('Segmentation Mode, After pointing three/more points press enter to release', 'segmentation');
}
startBboxDrawing(pos) {
this.setAnnotationMode('bbox');
this.startX = pos.x;
this.startY = pos.y;
this.isDrawing = true;
this.currentBox = { left: this.startX, top: this.startY, width: 0, height: 0 };
this.showModeIndicator('Bounding Box Mode', 'bbox');
}
setAnnotationMode(mode) {
this.annotationMode = mode;
document.getElementById('annotationMode').value = mode;
this.updateCanvasCursor();
}
showModeIndicator(text, type, persistent = false) {
// Create or update mode indicator
let indicator = document.getElementById('modeIndicator');
if (!indicator) {
indicator = document.createElement('div');
indicator.id = 'modeIndicator';
indicator.style.cssText = `
position: fixed;
top: 120px;
right: 20px;
z-index: 1001;
padding: 8px 16px;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
pointer-events: none;
transition: opacity 0.3s ease;
`;
document.body.appendChild(indicator);
}
const colors = {
'segmentation': { bg: '#e6f3ff', color: '#0066cc', border: '#4299e1' },
'bbox': { bg: '#f0fff4', color: '#22543d', border: '#48bb78' }
};
const color = colors[type] || colors.bbox;
indicator.style.background = color.bg;
indicator.style.color = color.color;
indicator.style.border = `1px solid ${color.border}`;
indicator.textContent = text;
indicator.style.opacity = '1';
// Auto-hide only if not persistent
if (!persistent) {
clearTimeout(this.modeIndicatorTimeout);
this.modeIndicatorTimeout = setTimeout(() => {
indicator.style.opacity = '0';
}, 2000);
}
}
onDoubleClick(e) {
if (this.annotationMode === 'segmentation' && this.isDrawingPolygon && this.polygonPoints.length >= 3) {
// Finish polygon
this.finishPolygon();
}
}
finishPolygon() {
if (this.polygonPoints.length >= 3) {
const annotation = {
type: 'segmentation',
points: [...this.polygonPoints],
classId: parseInt(document.getElementById('classId').value) || 0,
saved: false
};
this.annotations.push(annotation);
this.selectedBoxIndex = this.annotations.length - 1;
document.getElementById('boxCount').textContent = this.annotations.length;
this.updateSelectedBoxInfo();
}
this.isDrawingPolygon = false;
this.polygonPoints = [];
this.currentPolygon = null;
this.drawCanvas();
}
onMouseMove(e) {
if (!this.currentImage) return;
const pos = this.getMousePos(e);
// Handle dynamic annotation type detection
if (this.waitingForDrag && this.initialClickPos) {
const dragDistance = Math.sqrt(
Math.pow(pos.x - this.initialClickPos.x, 2) +
Math.pow(pos.y - this.initialClickPos.y, 2)
);
const timeSinceClick = Date.now() - this.clickStartTime;
// If dragging more than 10 pixels, assume bbox
if (dragDistance > 10) {
this.waitingForDrag = false;
this.startBboxDrawing(this.initialClickPos);
// Continue with bbox drawing
this.currentBox.width = pos.x - this.startX;
this.currentBox.height = pos.y - this.startY;
this.drawCanvas();
return;
}
// If holding click for more than 500ms without drag, assume segmentation
if (timeSinceClick > 500) {
this.waitingForDrag = false;
this.startSegmentation(this.initialClickPos);
return;
}
return; // Still waiting to determine type
}
// Rest of your existing mouse move logic...
if (this.annotationMode === 'segmentation') {
if (this.isDraggingEdge && this.selectedBoxIndex >= 0) {
this.resizePolygonEdge(this.selectedBoxIndex, this.draggingEdgeIndex, pos.x, pos.y);
this.drawCanvas();
return;
}
if (this.isDraggingPoint && this.selectedBoxIndex >= 0) {
this.movePolygonPoint(this.selectedBoxIndex, this.draggingPointIndex, pos.x, pos.y);
this.drawCanvas();
return;
}
if (this.isDragging && this.selectedBoxIndex >= 0) {
const deltaX = pos.x - this.lastMouseX;
const deltaY = pos.y - this.lastMouseY;
this.movePolygon(this.selectedBoxIndex, deltaX, deltaY);
this.lastMouseX = pos.x;
this.lastMouseY = pos.y;
this.drawCanvas();
return;
}
if (this.isDrawingPolygon) {
if (this.currentPolygon) {
this.currentPolygon.points = [...this.polygonPoints, pos];
this.drawCanvas();
}
return;
}
this.updatePolygonCursor(pos.x, pos.y);
return;
}
// Original bbox mouse move logic...
if (this.isResizing && this.selectedBoxIndex >= 0) {
this.resizeBox(pos.x, pos.y);
this.drawCanvas();
} else if (this.isDragging && this.selectedBoxIndex >= 0) {
const deltaX = pos.x - this.lastMouseX;
const deltaY = pos.y - this.lastMouseY;
this.moveBox(this.selectedBoxIndex, deltaX, deltaY);
this.lastMouseX = pos.x;
this.lastMouseY = pos.y;
this.drawCanvas();
} else if (this.isDrawing) {
this.currentBox.width = pos.x - this.startX;
this.currentBox.height = pos.y - this.startY;
this.drawCanvas();
} else {
this.updateCursor(pos.x, pos.y);
}
}
onMouseUp() {
// Handle waiting for drag detection
if (this.waitingForDrag) {
this.waitingForDrag = false;
// Short click without drag - default to segmentation
this.startSegmentation(this.initialClickPos);
return;
}
// Rest of your existing mouse up logic...
if (this.annotationMode === 'segmentation') {
this.isDraggingEdge = false;
this.draggingEdgeIndex = -1;
this.isDraggingPoint = false;
this.draggingPointIndex = -1;
this.isDragging = false;
this.canvas.style.cursor = 'crosshair';
return;
}
// Original bbox mouse up logic...
if (this.isDrawing && this.currentBox) {
if (Math.abs(this.currentBox.width) > 10 && Math.abs(this.currentBox.height) > 10) {
let left = Math.min(this.startX, this.startX + this.currentBox.width);
let top = Math.min(this.startY, this.startY + this.currentBox.height);
let width = Math.abs(this.currentBox.width);
let height = Math.abs(this.currentBox.height);
left = Math.max(0, left);
top = Math.max(0, top);
width = Math.min(width, this.originalWidth - left);
height = Math.min(height, this.originalHeight - top);
if (width > 10 && height > 10) {
const box = {
type: 'bbox',
left: left,
top: top,
width: width,
height: height,
classId: parseInt(document.getElementById('classId').value) || 0,
saved: false
};
this.annotations.push(box);
this.selectedBoxIndex = this.annotations.length - 1;
document.getElementById('boxCount').textContent = this.annotations.length;
this.updateSelectedBoxInfo();
}
}
this.currentBox = null;
}
this.isDrawing = false;
this.isDragging = false;
this.isResizing = false;
this.resizeHandle = null;
this.canvas.style.cursor = 'crosshair';
this.drawCanvas();
}
updatePolygonCursor(x, y) {
if (this.selectedBoxIndex >= 0 && this.annotations[this.selectedBoxIndex].type === 'segmentation') {
const handle = this.getResizeHandle(x, y);
this.canvas.style.cursor = handle.cursor;
return;
}
// Check for unselected polygons
const polygonIndex = this.getPolygonAtPosition(x, y);
if (polygonIndex >= 0) {
this.canvas.style.cursor = 'pointer';
} else {
this.canvas.style.cursor = 'crosshair';
}
}
updateCursor(x, y) {
if (this.selectedBoxIndex >= 0) {
const handle = this.getResizeHandle(x, y);
this.canvas.style.cursor = handle.cursor;
return;
}
const boxIndex = this.getBoxAtPosition(x, y);
if (boxIndex >= 0) {
this.canvas.style.cursor = 'pointer';
} else {
this.canvas.style.cursor = 'crosshair';
}
}
getResizeHandle(x, y) {
if (this.selectedBoxIndex < 0) return { cursor: 'move' };
const annotation = this.annotations[this.selectedBoxIndex];
// Handle polygon cursor logic with directional resize cursors
if (annotation.type === 'segmentation') {
const tolerance = 8;
// Check for corner points with directional resize cursors
const pointIndex = this.getPolygonPointAtPosition(x, y, tolerance);
if (pointIndex >= 0) {
const resizeCursor = this.getPolygonCornerResizeCursor(pointIndex, annotation.points);
return { cursor: resizeCursor, type: 'point', index: pointIndex };
}
// Check for edges with directional resize cursors
const edgeIndex = this.getPolygonEdgeAtPosition(x, y, 5);
if (edgeIndex >= 0) {
const edgeResizeCursor = this.getPolygonEdgeResizeCursor(edgeIndex, annotation.points);
return { cursor: edgeResizeCursor, type: 'edge', index: edgeIndex };
}
// Check if inside polygon (for moving entire polygon)
if (this.isPointInPolygon(x, y, annotation.points)) {
return { cursor: 'move', type: 'polygon-interior' };
}
// Default cursor for segmentation mode
return { cursor: 'crosshair', type: 'default' };
}
// Original bbox logic remains the same
const box = annotation;
const handleSize = 8;
const tolerance = 5;
const handles = [
{ x: box.left, y: box.top, cursor: 'nw-resize', type: 'nw' },
{ x: box.left + box.width, y: box.top, cursor: 'ne-resize', type: 'ne' },
{ x: box.left, y: box.top + box.height, cursor: 'sw-resize', type: 'sw' },
{ x: box.left + box.width, y: box.top + box.height, cursor: 'se-resize', type: 'se' },
{ x: box.left + box.width / 2, y: box.top, cursor: 'n-resize', type: 'n' },
{ x: box.left + box.width / 2, y: box.top + box.height, cursor: 's-resize', type: 's' },
{ x: box.left, y: box.top + box.height / 2, cursor: 'w-resize', type: 'w' },
{ x: box.left + box.width, y: box.top + box.height / 2, cursor: 'e-resize', type: 'e' }
];
for (let handle of handles) {
if (Math.abs(x - handle.x) <= tolerance && Math.abs(y - handle.y) <= tolerance) {
return handle;
}
}
// FIXED: Return proper object for interior clicks
if (x >= box.left && x <= box.left + box.width &&
y >= box.top && y <= box.top + box.height) {
return { cursor: 'move', type: 'interior' };
}
return { cursor: 'default', type: 'default' };
}
getPolygonCornerResizeCursor(pointIndex, points) {
if (points.length < 3) return 'move';
// Calculate polygon bounding box
const bounds = this.getPolygonBounds(points);
const currentPoint = points[pointIndex];
// Determine position relative to bounding box center
const centerX = bounds.left + bounds.width / 2;
const centerY = bounds.top + bounds.height / 2;
const isLeft = currentPoint.x < centerX;
const isRight = currentPoint.x > centerX;
const isTop = currentPoint.y < centerY;
const isBottom = currentPoint.y > centerY;
// Return appropriate resize cursor
if (isTop && isLeft) return 'nw-resize';
if (isTop && isRight) return 'ne-resize';
if (isBottom && isLeft) return 'sw-resize';
if (isBottom && isRight) return 'se-resize';
if (isTop) return 'n-resize';
if (isBottom) return 's-resize';
if (isLeft) return 'w-resize';
if (isRight) return 'e-resize';
return 'move'; // Center point or fallback
}
getPolygonBounds(points) {
if (points.length === 0) return { left: 0, top: 0, width: 0, height: 0 };
let minX = points[0].x, maxX = points[0].x;
let minY = points[0].y, maxY = points[0].y;
for (let i = 1; i < points.length; i++) {
minX = Math.min(minX, points[i].x);
maxX = Math.max(maxX, points[i].x);
minY = Math.min(minY, points[i].y);
maxY = Math.max(maxY, points[i].y);
}
return {
left: minX,
top: minY,
width: maxX - minX,
height: maxY - minY
};
}
getPolygonEdgeResizeCursor(edgeIndex, points) {
if (points.length < 2) return 'move';
// Get the two points that form this edge
const point1 = points[edgeIndex];
const point2 = points[(edgeIndex + 1) % points.length];
// Calculate edge direction
const deltaX = Math.abs(point2.x - point1.x);
const deltaY = Math.abs(point2.y - point1.y);
// Determine if edge is more horizontal or vertical
if (deltaX > deltaY) {
// More horizontal edge - allow vertical resizing
return 'ns-resize';
} else {
// More vertical edge - allow horizontal resizing
return 'ew-resize';
}
}
getBoxAtPosition(x, y) {
for (let i = this.annotations.length - 1; i >= 0; i--) {
const box = this.annotations[i];
if (x >= box.left && x <= box.left + box.width &&
y >= box.top && y <= box.top + box.height) {
return i;
}
}
return -1;
}
resizeBox(x, y) {
if (this.selectedBoxIndex < 0 || !this.resizeHandle) return;
const box = this.annotations[this.selectedBoxIndex];
const handle = this.resizeHandle;
switch (handle.type) {
case 'nw':
const newWidth = box.width + (box.left - x);
const newHeight = box.height + (box.top - y);
const newLeft = Math.max(0, x);
const newTop = Math.max(0, y);
if (newWidth > 10 && newHeight > 10) {
box.width = Math.min(newWidth, box.left + box.width);
box.height = Math.min(newHeight, box.top + box.height);
box.left = newLeft;
box.top = newTop;
}
break;
case 'ne':
const neWidth = Math.min(x - box.left, this.originalWidth - box.left);
const neHeight = box.height + (box.top - y);
const neTop = Math.max(0, y);
if (neWidth > 10 && neHeight > 10) {
box.width = neWidth;
box.height = Math.min(neHeight, box.top + box.height);
box.top = neTop;
}
break;
case 'sw':
const swWidth = box.width + (box.left - x);
const swHeight = Math.min(y - box.top, this.originalHeight - box.top);
const swLeft = Math.max(0, x);
if (swWidth > 10 && swHeight > 10) {
box.width = Math.min(swWidth, box.left + box.width);
box.height = swHeight;
box.left = swLeft;
}
break;
case 'se':
const seWidth = Math.min(x - box.left, this.originalWidth - box.left);
const seHeight = Math.min(y - box.top, this.originalHeight - box.top);
if (seWidth > 10 && seHeight > 10) {
box.width = seWidth;
box.height = seHeight;
}
break;
case 'n':
const nHeight = box.height + (box.top - y);
const nTop = Math.max(0, y);
if (nHeight > 10) {
box.height = Math.min(nHeight, box.top + box.height);
box.top = nTop;
}
break;
case 's':
const sHeight = Math.min(y - box.top, this.originalHeight - box.top);
if (sHeight > 10) {
box.height = sHeight;
}
break;
case 'w':
const wWidth = box.width + (box.left - x);
const wLeft = Math.max(0, x);
if (wWidth > 10) {
box.width = Math.min(wWidth, box.left + box.width);
box.left = wLeft;
}
break;
case 'e':
const eWidth = Math.min(x - box.left, this.originalWidth - box.left);
if (eWidth > 10) {
box.width = eWidth;
}
break;
}
box.saved = false;
this.updateSelectedBoxInfo();
}
moveBox(boxIndex, deltaX, deltaY) {
if (boxIndex < 0 || boxIndex >= this.annotations.length) return;
const box = this.annotations[boxIndex];
// Calculate new position
let newLeft = box.left + deltaX;
let newTop = box.top + deltaY;
// Apply boundary conditions
newLeft = Math.max(0, Math.min(this.originalWidth - box.width, newLeft));
newTop = Math.max(0, Math.min(this.originalHeight - box.height, newTop));
// Update box position
box.left = newLeft;
box.top = newTop;
box.saved = false;
this.updateSelectedBoxInfo();
}
// Modified onKeyDown method with new resize functionality
onKeyDown(e) {
// Handle escape key for canceling drawing
if (e.key === 'Escape') {
e.preventDefault();
// Cancel polygon drawing if in progress
if (this.annotationMode === 'segmentation' && this.isDrawingPolygon) {
this.cancelPolygonDrawing();
return;
}
// Cancel bbox drawing if in progress
if (this.annotationMode === 'bbox' && this.isDrawing) {
this.cancelBboxDrawing();
return;
}
// Clear selection if nothing is being drawn
this.selectedBoxIndex = -1;
this.updateSelectedBoxInfo();
this.drawCanvas();
return;
}
if (e.key === 'Enter') {
e.preventDefault();
this.onDoubleClick()
return;
}
if (!this.currentImage || this.selectedBoxIndex < 0) return;
const resizeDistance = 5; // Fixed resize distance
const moveDistance = e.shiftKey ? 10 : 1;
if (e.shiftKey) {
// Shift + arrow keys for resizing
switch (e.key) {
case 'ArrowLeft':
e.preventDefault();
this.resizeSelectedBox(-resizeDistance, 0);
break;
case 'ArrowRight':
e.preventDefault();
this.resizeSelectedBox(resizeDistance, 0);
break;
case 'ArrowUp':
e.preventDefault();
this.resizeSelectedBox(0, -resizeDistance);
break;
case 'ArrowDown':
e.preventDefault();
this.resizeSelectedBox(0, resizeDistance);
break;
}
} else {
// Regular arrow keys for moving
switch (e.key) {
case 'ArrowLeft':
e.preventDefault();
this.moveBox(this.selectedBoxIndex, -moveDistance, 0);
this.drawCanvas();
break;
case 'ArrowRight':
e.preventDefault();
this.moveBox(this.selectedBoxIndex, moveDistance, 0);
this.drawCanvas();
break;
case 'ArrowUp':
e.preventDefault();
this.moveBox(this.selectedBoxIndex, 0, -moveDistance);
this.drawCanvas();
break;
case 'ArrowDown':
e.preventDefault();
this.moveBox(this.selectedBoxIndex, 0, moveDistance);
this.drawCanvas();
break;
}
}
// Other key handlers
switch (e.key) {
case 'Delete':
// case 'Backspace':
e.preventDefault();
this.deleteSelectedBox();
break;
case 'Escape':
e.preventDefault();
this.selectedBoxIndex = -1;
this.updateSelectedBoxInfo();
this.drawCanvas();
break;
}
}
cancelPolygonDrawing() {
this.isDrawingPolygon = false;
this.polygonPoints = [];
this.currentPolygon = null;
this.selectedBoxIndex = -1;
this.updateSelectedBoxInfo();
this.drawCanvas();
this.showAlert('Polygon drawing canceled', 'info');
}
cancelBboxDrawing() {
this.isDrawing = false;
this.currentBox = null;
this.selectedBoxIndex = -1;
this.updateSelectedBoxInfo();
this.drawCanvas();
this.showAlert('Bounding box drawing canceled', 'info');
}
// New method to resize selected box
resizeSelectedBox(deltaWidth, deltaHeight) {
if (this.selectedBoxIndex < 0) return;
const box = this.annotations[this.selectedBoxIndex];
// Calculate new dimensions
let newWidth = box.width + deltaWidth;
let newHeight = box.height + deltaHeight;
// Apply boundary conditions for width
if (newWidth >= 10) {
// Ensure box doesn't exceed original width
newWidth = Math.min(newWidth, this.originalWidth - box.left);
box.width = newWidth;
// Adjust left position if needed
if (box.left + box.width > this.originalWidth) {
box.left = this.originalWidth - box.width;
}
}
// Apply boundary conditions for height
if (newHeight >= 10) {
// Ensure box doesn't exceed original height
newHeight = Math.min(newHeight, this.originalHeight - box.top);
box.height = newHeight;
// Adjust top position if needed
if (box.top + box.height > this.originalHeight) {
box.top = this.originalHeight - box.height;
}
}
box.saved = false;
this.updateSelectedBoxInfo();
this.drawCanvas();
}
deleteSelectedBox() {
if (this.selectedBoxIndex >= 0) {
this.annotations.splice(this.selectedBoxIndex, 1);
this.selectedBoxIndex = -1;
document.getElementById('boxCount').textContent = this.annotations.length;
this.updateSelectedBoxInfo();
this.drawCanvas();
// this.showAlert('Box deleted', 'info');
}
}
// Enhanced updateSelectedBoxInfo method
updateSelectedBoxInfo() {
const selectedBoxInfo = document.getElementById('selectedBoxInfo');
const editPanel = document.getElementById('annotationEditPanel');
if (this.selectedBoxIndex >= 0) {
const annotation = this.annotations[this.selectedBoxIndex];
if (annotation.type === 'segmentation') {
const bounds = this.getPolygonBounds(annotation.points);
const pointCount = annotation.points.length;
selectedBoxInfo.textContent = `#${this.selectedBoxIndex + 1} Polygon (${pointCount} points, ${Math.round(bounds.left)}, ${Math.round(bounds.top)}, ${Math.round(bounds.width)}×${Math.round(bounds.height)})`;
} else {
selectedBoxInfo.textContent = `#${this.selectedBoxIndex + 1} BBox (${Math.round(annotation.left)}, ${Math.round(annotation.top)}, ${Math.round(annotation.width)}×${Math.round(annotation.height)})`;
}
// Show and populate edit panel
this.showEditPanel(annotation);
editPanel.style.display = 'block';
} else {
selectedBoxInfo.textContent = 'None';
editPanel.style.display = 'none';
}
}
// New method to show edit panel
showEditPanel(annotation) {
const editType = document.getElementById('editType');
const editClassId = document.getElementById('editClassId');
const bboxControls = document.getElementById('bboxEditControls');
const polygonControls = document.getElementById('polygonEditControls');
// Set common fields
editType.textContent = annotation.type === 'segmentation' ? 'Polygon' : 'Bounding Box';
editClassId.value = annotation.classId || 0;
if (annotation.type === 'bbox') {
// Show bbox controls
bboxControls.style.display = 'block';
polygonControls.style.display = 'none';
document.getElementById('editLeft').value = Math.round(annotation.left);
document.getElementById('editTop').value = Math.round(annotation.top);
document.getElementById('editWidth').value = Math.round(annotation.width);
document.getElementById('editHeight').value = Math.round(annotation.height);
// Set max values based on current position
document.getElementById('editLeft').max = this.originalWidth - annotation.width;
document.getElementById('editTop').max = this.originalHeight - annotation.height;
document.getElementById('editWidth').max = this.originalWidth - annotation.left;
document.getElementById('editHeight').max = this.originalHeight - annotation.top;
} else {
// Show polygon controls
bboxControls.style.display = 'none';
polygonControls.style.display = 'block';
this.populatePolygonPointsList(annotation.points);
}
}
// New method to populate polygon points list
// Enhanced method to populate polygon points list with hover highlighting
populatePolygonPointsList(points) {
const pointsList = document.getElementById('pointsList');
const pointCount = document.getElementById('pointCount');
pointCount.textContent = points.length;
pointsList.innerHTML = '';
points.forEach((point, index) => {
const row = document.createElement('div');
row.className = 'point-edit-row';
row.setAttribute('data-point-index', index);
row.innerHTML = `
<span style="font-size: 15px; color: #4a5568; min-width: 15px;">${index + 1}:</span>
<input type="number" class="point-edit-input" value="${Math.round(point.x)}"
data-index="${index}" data-coord="x" min="0" max="${this.originalWidth}">
<input type="number" class="point-edit-input" value="${Math.round(point.y)}"
data-index="${index}" data-coord="y" min="0" max="${this.originalHeight}">
<button class="point-delete-btn" data-index="${index}"
${points.length <= 3 ? 'disabled title="Cannot delete - minimum 3 points required"' : ''}>
×
</button>
`;
pointsList.appendChild(row);
});
// Add event listeners for point inputs
pointsList.querySelectorAll('.point-edit-input').forEach(input => {
input.addEventListener('change', (e) => this.updatePolygonPoint(e));
// Add hover event listeners for highlighting
input.addEventListener('mouseenter', (e) => this.highlightPolygonPoint(e));
input.addEventListener('mouseleave', (e) => this.unhighlightPolygonPoint(e));
input.addEventListener('focus', (e) => this.highlightPolygonPoint(e));
input.addEventListener('blur', (e) => this.unhighlightPolygonPoint(e));
});
// Add event listeners for delete buttons
pointsList.querySelectorAll('.point-delete-btn').forEach(btn => {
btn.addEventListener('click', (e) => this.deletePolygonPoint(e));
// Add hover event listeners for delete buttons
btn.addEventListener('mouseenter', (e) => this.highlightPolygonPoint(e));
btn.addEventListener('mouseleave', (e) => this.unhighlightPolygonPoint(e));
});
// Add hover listeners for entire rows
pointsList.querySelectorAll('.point-edit-row').forEach(row => {
row.addEventListener('mouseenter', (e) => this.highlightPolygonPointByRow(e));
row.addEventListener('mouseleave', (e) => this.unhighlightPolygonPointByRow(e));
});
}
// Method to highlight a polygon point on canvas
highlightPolygonPoint(e) {
const index = parseInt(e.target.dataset.index);
if (this.selectedBoxIndex >= 0 && index >= 0) {
this.highlightedPointIndex = index;
// Highlight the row in sidebar
const row = e.target.closest('.point-edit-row');
if (row) {
row.classList.add('highlighted');
row.querySelectorAll('.point-edit-input').forEach(input => {
input.classList.add('highlighted');
});
}
this.drawCanvas();
}
}
// Method to unhighlight a polygon point
unhighlightPolygonPoint(e) {
this.highlightedPointIndex = -1;
// Remove highlight from sidebar row
const row = e.target.closest('.point-edit-row');
if (row) {
row.classList.remove('highlighted');
row.querySelectorAll('.point-edit-input').forEach(input => {
input.classList.remove('highlighted');
});
}
this.drawCanvas();
}
// Method to highlight by hovering over entire row
highlightPolygonPointByRow(e) {
const row = e.currentTarget;
const index = parseInt(row.dataset.pointIndex);
if (index >= 0) {
this.highlightedPointIndex = index;
row.classList.add('highlighted');
row.querySelectorAll('.point-edit-input').forEach(input => {
input.classList.add('highlighted');
});
this.drawCanvas();
}
}
// Method to unhighlight by leaving row
unhighlightPolygonPointByRow(e) {
const row = e.currentTarget;
this.highlightedPointIndex = -1;
row.classList.remove('highlighted');
row.querySelectorAll('.point-edit-input').forEach(input => {
input.classList.remove('highlighted');
});
this.drawCanvas();
}
// New method to update polygon point
updatePolygonPoint(e) {
const index = parseInt(e.target.dataset.index);
const coord = e.target.dataset.coord;
const value = parseInt(e.target.value);
if (this.selectedBoxIndex >= 0) {
const annotation = this.annotations[this.selectedBoxIndex];
if (annotation.type === 'segmentation' && annotation.points[index]) {
annotation.points[index][coord] = Math.max(0,
Math.min(coord === 'x' ? this.originalWidth : this.originalHeight, value));
annotation.saved = false;
this.drawCanvas();
}
}
}
// New method to delete polygon point
deletePolygonPoint(e) {
const index = parseInt(e.target.dataset.index);
if (this.selectedBoxIndex >= 0) {
const annotation = this.annotations[this.selectedBoxIndex];
if (annotation.type === 'segmentation' && annotation.points.length > 3) {
annotation.points.splice(index, 1);
annotation.saved = false;
this.populatePolygonPointsList(annotation.points);
this.updateSelectedBoxInfo();
this.drawCanvas();
}
}
}
// Method to apply edits
applyEdits() {
if (this.selectedBoxIndex >= 0) {
const annotation = this.annotations[this.selectedBoxIndex];
const newClassId = parseInt(document.getElementById('editClassId').value) || 0;
// Update class ID
annotation.classId = newClassId;
if (annotation.type === 'bbox') {
// Update bbox properties
const newLeft = parseInt(document.getElementById('editLeft').value);
const newTop = parseInt(document.getElementById('editTop').value);
const newWidth = parseInt(document.getElementById('editWidth').value);
const newHeight = parseInt(document.getElementById('editHeight').value);
// Apply boundary constraints
annotation.left = Math.max(0, Math.min(this.originalWidth - newWidth, newLeft));
annotation.top = Math.max(0, Math.min(this.originalHeight - newHeight, newTop));
annotation.width = Math.max(10, Math.min(this.originalWidth - annotation.left, newWidth));
annotation.height = Math.max(10, Math.min(this.originalHeight - annotation.top, newHeight));
}
annotation.saved = false;
this.updateSelectedBoxInfo();
this.drawCanvas();
this.showAlert('Annotation updated', 'success');
}
}
// Method to cancel edits
cancelEdits() {
// Just refresh the edit panel with current values
if (this.selectedBoxIndex >= 0) {
this.showEditPanel(this.annotations[this.selectedBoxIndex]);
}
}
// Method for real-time bbox updates
updateBboxFromInputs() {
if (this.selectedBoxIndex >= 0) {
const annotation = this.annotations[this.selectedBoxIndex];
if (annotation.type === 'bbox') {
const newLeft = parseInt(document.getElementById('editLeft').value) || 0;
const newTop = parseInt(document.getElementById('editTop').value) || 0;
const newWidth = parseInt(document.getElementById('editWidth').value) || 10;
const newHeight = parseInt(document.getElementById('editHeight').value) || 10;
// Apply constraints and update
annotation.left = Math.max(0, Math.min(this.originalWidth - newWidth, newLeft));
annotation.top = Math.max(0, Math.min(this.originalHeight - newHeight, newTop));
annotation.width = Math.max(10, Math.min(this.originalWidth - annotation.left, newWidth));
annotation.height = Math.max(10, Math.min(this.originalHeight - annotation.top, newHeight));
annotation.saved = false;
this.drawCanvas();
}
}
}
getMousePos(e) {
const rect = this.canvas.getBoundingClientRect();
const scaleX = this.canvas.width / rect.width;
const scaleY = this.canvas.height / rect.height;
return {
x: (e.clientX - rect.left) * scaleX,
y: (e.clientY - rect.top) * scaleY
};
}
// Update the save method to handle new format
async saveAnnotations() {
if (!this.currentImage || this.annotations.length === 0) {
this.showAlert('No annotations to save', 'info');
return;
}
try {
const response = await fetch('/api/annotate/annotations', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
annotations: this.annotations.map(ann => ({ ...ann, saved: true })),
image_name: this.currentImage,
original_width: this.originalWidth,
original_height: this.originalHeight
})
});
if (response.ok) {
const result = await response.json();
this.annotations.forEach(box => box.saved = true);
this.drawCanvas();
this.showAlert(result.message, 'success');
this.loadImages();
} else {
throw new Error('Failed to save annotations');
}
} catch (error) {
this.showAlert('Error saving annotations: ' + error.message, 'error');
}
}
undoLastBox() {
if (this.annotations.length > 0) {
this.annotations.pop();
this.selectedBoxIndex = -1;
document.getElementById('boxCount').textContent = this.annotations.length;
this.updateSelectedBoxInfo();
this.drawCanvas();
this.showAlert('Last box removed', 'info');
} else {
this.showAlert('No annotations to undo', 'info');
}
}
async clearAllAnnotations() {
if (!this.currentImage) return;
if (confirm('Are you sure you want to clear all annotations and delete the annotation file?')) {
try {
// Delete annotations from server
await fetch(`/api/annotate/annotations/${encodeURIComponent(this.currentImage)}`, {
method: 'DELETE'
});
this.annotations = [];
this.selectedBoxIndex = -1;
document.getElementById('boxCount').textContent = '0';
this.updateSelectedBoxInfo();
this.drawCanvas();
this.showAlert('All annotations cleared', 'success');
this.loadImages(); // Refresh progress
} catch (error) {
this.showAlert('Error clearing annotations: ' + error.message, 'error');
}
}
}
async reloadAnnotations() {
if (!this.currentImage) return;
try {
const response = await fetch(`/api/annotate/annotations/${encodeURIComponent(this.currentImage)}`);
const data = await response.json();
this.annotations = (data.annotations || []).map(box => ({
...box,
saved: true
}));
this.selectedBoxIndex = -1;
document.getElementById('boxCount').textContent = this.annotations.length;
this.updateSelectedBoxInfo();
this.drawCanvas();
this.showAlert('Annotations reloaded from file', 'success');
} catch (error) {
this.showAlert('Error reloading annotations: ' + error.message, 'error');
}
}
async detectAnnotations() {
if (!this.currentImage) return;
try {
const response = await fetch(`/api/annotate/detect_annotations/${encodeURIComponent(this.currentImage)}`);
const data = await response.json();
this.annotations = (data.annotations || []).map(box => ({
...box,
saved: true
}));
this.selectedBoxIndex = -1;
document.getElementById('boxCount').textContent = this.annotations.length;
this.updateSelectedBoxInfo();
this.drawCanvas();
this.showAlert('Annotations detected and loaded from file', 'success');
} catch (error) {
this.showAlert('Error reloading annotations: ' + error.message, 'error');
}
}
downloadAnnotations() {
if (!this.currentImage) return;
const link = document.createElement('a');
link.href = `//annotate/annotations/${encodeURIComponent(this.currentImage)}/download`;
link.download = `${this.currentImage.split('.')[0]}.txt`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
async uploadImage(file) {
const formData = new FormData();
formData.append('file', file);
try {
this.showModeIndicator('File Upload is in progress', 'Uploading File', true);
const response = await fetch('/api/annotate/upload', {
method: 'POST',
body: formData
});
if (response.ok) {
const result = await response.json();
await this.loadImages();
// Auto-select the uploaded image
var new_file_name = file.name
const index = this.images.findIndex(img => img.name === new_file_name);
if (index >= 0) {
this.currentImageIndex = index;
document.getElementById('imageSelect').value = new_file_name;
this.showModeIndicator(result.message, 'Uploading File');
this.loadImage(new_file_name);
}
} else {
throw new Error('Upload failed');
}
} catch (error) {
this.showAlert('Error uploading image: ' + error.message, 'error');
}
// Reset file input
document.getElementById('uploadFile').value = '';
}
showAlert(message, type) {
const alertsContainer = document.getElementById('alerts');
const alert = document.createElement('div');
alert.className = `alert alert-${type}`;
alert.textContent = message;
alertsContainer.appendChild(alert);
// Auto-remove alert after 5 seconds
setTimeout(() => {
if (alert.parentNode) {
alert.parentNode.removeChild(alert);
}
}, 5000);
}
////////////////////////// ----train---- //////////////////////////
openXterm() {
const modal = document.getElementById('outputModal');
modal.style.display = 'block';
// Initialize terminal on first run
if (!term) {
term = new Terminal({
cursorBlink: true,
convertEol: true,
theme: {
background: '#000000',
foreground: '#00FF7F', // SpringGreen
cursor: 'rgba(255, 255, 255, 0.5)'
}
});
fitAddon = new FitAddon.FitAddon();
const webLinksAddon = new WebLinksAddon.WebLinksAddon();
term.loadAddon(fitAddon);
term.loadAddon(webLinksAddon);
term.open(document.getElementById('output'));
}
// Use a short timeout to ensure the modal is visible before fitting
setTimeout(() => fitAddon.fit(), 50);
term.clear();
term.focus();
term.write('\x1b[33mRunning command...\x1b[0m\r\n');
}
clearOutput() {
if (term) {
term.clear();
}
}
stopTrain() {
fetch('/api/annotate/stopTrain', {
method: 'GET',
headers: { 'Content-Type': 'application/json' }
})
}
closeTrainModal() {
const modal = document.getElementById('outputModal');
modal.style.display = 'none';
}
}
// Initialize the application when the page loads
document.addEventListener('DOMContentLoaded', () => {
new ComicAnnotator();
});
</script>
</body>
</html>