jebin2 commited on
Commit
a1f4a1e
·
1 Parent(s): fef987b

added polygon support

Browse files
comic_panel_extractor/annorator_server.py CHANGED
@@ -8,6 +8,7 @@ import base64
8
  from io import BytesIO
9
  import shutil
10
  from .config import Config
 
11
 
12
  app = APIRouter()
13
 
@@ -19,12 +20,21 @@ IMAGE_LABEL_ROOT = os.path.join(Config.current_path, "image_labels")
19
  CLASS_ID = 0
20
 
21
  # === Pydantic Models ===
 
 
 
 
22
  class Box(BaseModel):
23
- left: int
24
- top: int
25
- width: int
26
- height: int
27
- type: str = "rect"
 
 
 
 
 
28
  stroke: str = "#00ff00"
29
  strokeWidth: int = 3
30
  fill: str = "rgba(0, 255, 0, 0.2)"
@@ -32,11 +42,11 @@ class Box(BaseModel):
32
 
33
  @field_validator("left", "top", "width", "height", mode="before")
34
  def round_floats(cls, v):
35
- return round(v)
36
 
37
  class SaveAnnotationsRequest(BaseModel):
38
- boxes: List[Box]
39
- image_name: str # Relative path like train/image1.jpg
40
  original_width: int
41
  original_height: int
42
 
@@ -54,65 +64,162 @@ def get_label_path(image_name: str) -> str:
54
  return os.path.join(LABEL_ROOT, os.path.splitext(image_name)[0] + ".txt")
55
 
56
  # === Core Functions ===
57
- def load_yolo_boxes(image_path: str, label_path: str, detect: bool = False):
 
58
  try:
59
  img = Image.open(image_path)
60
  w, h = img.size
61
- boxes = []
 
 
62
  if detect and not os.path.exists(label_path):
63
  from .yolo_manager import YOLOManager
64
  with YOLOManager() as yolo_manager:
65
  weights_path = f'{Config.current_path}/{Config.YOLO_MODEL_NAME}.pt'
66
-
67
  yolo_manager.load_model(weights_path)
68
-
69
- # Run inference
70
- _, label_path = yolo_manager.annotate_images(image_paths=[image_path], output_dir=IMAGE_LABEL_ROOT, save_image=False, label_path=label_path)
 
 
 
71
 
72
  if os.path.exists(label_path):
73
  with open(label_path, "r") as f:
74
  for line in f:
75
  parts = list(map(float, line.strip().split()))
76
- if len(parts) != 5:
77
  continue
78
- _, xc, yc, bw, bh = parts
79
- left = int((xc - bw / 2) * w)
80
- top = int((yc - bh / 2) * h)
81
- width = int(bw * w)
82
- height = int(bh * h)
83
- boxes.append({
84
- "type": "rect",
85
- "left": left,
86
- "top": top,
87
- "width": width,
88
- "height": height,
89
- "stroke": "#00ff00",
90
- "strokeWidth": 3,
91
- "fill": "rgba(0, 255, 0, 0.2)",
92
- "saved": True
93
- })
94
- return boxes, (w, h)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
  except Exception as e:
96
- raise HTTPException(status_code=500, detail=f"Error loading data: {str(e)}")
97
 
98
- def save_yolo_annotations(boxes: List[Box], original_size: tuple, label_path: str):
 
99
  os.makedirs(os.path.dirname(label_path), exist_ok=True)
100
  w, h = original_size
 
101
  try:
102
  with open(label_path, "w") as f:
103
- for box in boxes:
104
- left, top, width, height = box.left, box.top, box.width, box.height
105
- xc = (left + width / 2) / w
106
- yc = (top + height / 2) / h
107
- bw = width / w
108
- bh = height / h
109
- f.write(f"{CLASS_ID} {xc:.6f} {yc:.6f} {bw:.6f} {bh:.6f}\n")
110
-
 
 
 
 
 
 
 
 
 
 
 
 
111
  shutil.copy2(label_path, f"{IMAGE_LABEL_ROOT}/{os.path.basename(label_path)}")
112
  return True
113
  except Exception as e:
114
  raise HTTPException(status_code=500, detail=f"Error saving annotations: {str(e)}")
115
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
  # === API Routes ===
117
 
118
  @app.get("/api/annotate/images", response_model=List[ImageInfo])
@@ -162,24 +269,24 @@ async def get_annotations(image_name: str):
162
  if not os.path.exists(image_path):
163
  raise HTTPException(status_code=404, detail="Image not found")
164
 
165
- boxes, (width, height) = load_yolo_boxes(image_path, label_path)
166
  return {
167
- "boxes": boxes,
168
  "original_width": width,
169
  "original_height": height
170
  }
171
 
172
  @app.get("/api/annotate/detect_annotations/{image_name:path}")
173
- async def get_annotations(image_name: str):
174
  image_path = get_image_path(image_name)
175
  label_path = get_label_path(image_name)
176
 
177
  if not os.path.exists(image_path):
178
  raise HTTPException(status_code=404, detail="Image not found")
179
 
180
- boxes, (width, height) = load_yolo_boxes(image_path, label_path, True)
181
  return {
182
- "boxes": boxes,
183
  "original_width": width,
184
  "original_height": height
185
  }
@@ -188,11 +295,11 @@ async def get_annotations(image_name: str):
188
  async def save_annotations(request: SaveAnnotationsRequest):
189
  label_path = get_label_path(request.image_name)
190
  success = save_yolo_annotations(
191
- request.boxes,
192
  (request.original_width, request.original_height),
193
  label_path
194
  )
195
- return {"message": f"Saved {len(request.boxes)} annotations successfully"}
196
 
197
  @app.delete("/api/annotate/annotations/{image_name:path}")
198
  async def delete_annotations(image_name: str):
 
8
  from io import BytesIO
9
  import shutil
10
  from .config import Config
11
+ from typing import List, Optional, Union, Dict, Any
12
 
13
  app = APIRouter()
14
 
 
20
  CLASS_ID = 0
21
 
22
  # === Pydantic Models ===
23
+ class Point(BaseModel):
24
+ x: float
25
+ y: float
26
+
27
  class Box(BaseModel):
28
+ type: str = "bbox" # "bbox" or "segmentation"
29
+ # For bbox
30
+ left: Optional[int] = None
31
+ top: Optional[int] = None
32
+ width: Optional[int] = None
33
+ height: Optional[int] = None
34
+ # For segmentation
35
+ points: Optional[List[Point]] = None
36
+ # Common fields
37
+ classId: int = CLASS_ID
38
  stroke: str = "#00ff00"
39
  strokeWidth: int = 3
40
  fill: str = "rgba(0, 255, 0, 0.2)"
 
42
 
43
  @field_validator("left", "top", "width", "height", mode="before")
44
  def round_floats(cls, v):
45
+ return round(v) if v is not None else None
46
 
47
  class SaveAnnotationsRequest(BaseModel):
48
+ annotations: List[Box] # Changed from 'boxes' to 'annotations'
49
+ image_name: str
50
  original_width: int
51
  original_height: int
52
 
 
64
  return os.path.join(LABEL_ROOT, os.path.splitext(image_name)[0] + ".txt")
65
 
66
  # === Core Functions ===
67
+ def load_yolo_annotations(image_path: str, label_path: str, detect: bool = False):
68
+ """Load both bbox and segmentation annotations from YOLO format"""
69
  try:
70
  img = Image.open(image_path)
71
  w, h = img.size
72
+ annotations = []
73
+
74
+ # Auto-detect if needed
75
  if detect and not os.path.exists(label_path):
76
  from .yolo_manager import YOLOManager
77
  with YOLOManager() as yolo_manager:
78
  weights_path = f'{Config.current_path}/{Config.YOLO_MODEL_NAME}.pt'
 
79
  yolo_manager.load_model(weights_path)
80
+ _, label_path = yolo_manager.annotate_images(
81
+ image_paths=[image_path],
82
+ output_dir=IMAGE_LABEL_ROOT,
83
+ save_image=False,
84
+ label_path=label_path
85
+ )
86
 
87
  if os.path.exists(label_path):
88
  with open(label_path, "r") as f:
89
  for line in f:
90
  parts = list(map(float, line.strip().split()))
91
+ if len(parts) < 5:
92
  continue
93
+
94
+ class_id = int(parts[0])
95
+
96
+ if len(parts) == 5: # Bounding box format
97
+ _, xc, yc, bw, bh = parts
98
+ left = int((xc - bw / 2) * w)
99
+ top = int((yc - bh / 2) * h)
100
+ width = int(bw * w)
101
+ height = int(bh * h)
102
+
103
+ annotations.append({
104
+ "type": "bbox",
105
+ "left": left,
106
+ "top": top,
107
+ "width": width,
108
+ "height": height,
109
+ "classId": class_id,
110
+ "stroke": "#00ff00",
111
+ "strokeWidth": 3,
112
+ "fill": "rgba(0, 255, 0, 0.2)",
113
+ "saved": True
114
+ })
115
+
116
+ elif len(parts) > 5 and len(parts) % 2 == 1: # Segmentation format
117
+ # Skip class_id, then pairs of x,y coordinates
118
+ coords = parts[1:]
119
+ if len(coords) >= 6: # At least 3 points
120
+ points = []
121
+ for i in range(0, len(coords), 2):
122
+ if i + 1 < len(coords):
123
+ x = coords[i] * w
124
+ y = coords[i + 1] * h
125
+ points.append({"x": x, "y": y})
126
+
127
+ annotations.append({
128
+ "type": "segmentation",
129
+ "points": points,
130
+ "classId": class_id,
131
+ "stroke": "#00ff00",
132
+ "strokeWidth": 3,
133
+ "fill": "rgba(0, 255, 0, 0.2)",
134
+ "saved": True
135
+ })
136
+
137
+ return annotations, (w, h)
138
  except Exception as e:
139
+ raise HTTPException(status_code=500, detail=f"Error loading annotations: {str(e)}")
140
 
141
+ def save_yolo_annotations(annotations: List[Box], original_size: tuple, label_path: str):
142
+ """Save annotations in YOLO format (both bbox and segmentation)"""
143
  os.makedirs(os.path.dirname(label_path), exist_ok=True)
144
  w, h = original_size
145
+
146
  try:
147
  with open(label_path, "w") as f:
148
+ # Generate YOLO format from annotations
149
+ for annotation in annotations:
150
+ if annotation.type == "bbox":
151
+ left, top, width, height = annotation.left, annotation.top, annotation.width, annotation.height
152
+ xc = (left + width / 2) / w
153
+ yc = (top + height / 2) / h
154
+ bw = width / w
155
+ bh = height / h
156
+ f.write(f"{annotation.classId} {xc:.6f} {yc:.6f} {bw:.6f} {bh:.6f}\n")
157
+
158
+ elif annotation.type == "segmentation" and annotation.points:
159
+ # Convert points to normalized coordinates
160
+ normalized_points = []
161
+ for point in annotation.points:
162
+ normalized_points.extend([point.x / w, point.y / h])
163
+
164
+ coords_str = " ".join(f"{coord:.6f}" for coord in normalized_points)
165
+ f.write(f"{annotation.classId} {coords_str}\n")
166
+
167
+ # Copy to image_labels directory
168
  shutil.copy2(label_path, f"{IMAGE_LABEL_ROOT}/{os.path.basename(label_path)}")
169
  return True
170
  except Exception as e:
171
  raise HTTPException(status_code=500, detail=f"Error saving annotations: {str(e)}")
172
 
173
+ def parse_yolo_line(line: str, image_width: int, image_height: int) -> Dict[str, Any]:
174
+ """Parse a single YOLO format line and return annotation dict"""
175
+ parts = list(map(float, line.strip().split()))
176
+ if len(parts) < 5:
177
+ return None
178
+
179
+ class_id = int(parts[0])
180
+
181
+ if len(parts) == 5: # Bounding box
182
+ _, xc, yc, bw, bh = parts
183
+ left = int((xc - bw / 2) * image_width)
184
+ top = int((yc - bh / 2) * image_height)
185
+ width = int(bw * image_width)
186
+ height = int(bh * image_height)
187
+
188
+ return {
189
+ "type": "bbox",
190
+ "left": left,
191
+ "top": top,
192
+ "width": width,
193
+ "height": height,
194
+ "classId": class_id,
195
+ "stroke": "#00ff00",
196
+ "strokeWidth": 3,
197
+ "fill": "rgba(0, 255, 0, 0.2)",
198
+ "saved": True
199
+ }
200
+
201
+ elif len(parts) > 5 and len(parts) % 2 == 1: # Segmentation
202
+ coords = parts[1:]
203
+ if len(coords) >= 6: # At least 3 points
204
+ points = []
205
+ for i in range(0, len(coords), 2):
206
+ if i + 1 < len(coords):
207
+ x = coords[i] * image_width
208
+ y = coords[i + 1] * image_height
209
+ points.append({"x": x, "y": y})
210
+
211
+ return {
212
+ "type": "segmentation",
213
+ "points": points,
214
+ "classId": class_id,
215
+ "stroke": "#00ff00",
216
+ "strokeWidth": 3,
217
+ "fill": "rgba(0, 255, 0, 0.2)",
218
+ "saved": True
219
+ }
220
+
221
+ return None
222
+
223
  # === API Routes ===
224
 
225
  @app.get("/api/annotate/images", response_model=List[ImageInfo])
 
269
  if not os.path.exists(image_path):
270
  raise HTTPException(status_code=404, detail="Image not found")
271
 
272
+ annotations, (width, height) = load_yolo_annotations(image_path, label_path)
273
  return {
274
+ "annotations": annotations, # Changed from "boxes"
275
  "original_width": width,
276
  "original_height": height
277
  }
278
 
279
  @app.get("/api/annotate/detect_annotations/{image_name:path}")
280
+ async def get_detected_annotations(image_name: str):
281
  image_path = get_image_path(image_name)
282
  label_path = get_label_path(image_name)
283
 
284
  if not os.path.exists(image_path):
285
  raise HTTPException(status_code=404, detail="Image not found")
286
 
287
+ annotations, (width, height) = load_yolo_annotations(image_path, label_path, True)
288
  return {
289
+ "annotations": annotations,
290
  "original_width": width,
291
  "original_height": height
292
  }
 
295
  async def save_annotations(request: SaveAnnotationsRequest):
296
  label_path = get_label_path(request.image_name)
297
  success = save_yolo_annotations(
298
+ request.annotations,
299
  (request.original_width, request.original_height),
300
  label_path
301
  )
302
+ return {"message": f"Saved {len(request.annotations)} annotations successfully"}
303
 
304
  @app.delete("/api/annotate/annotations/{image_name:path}")
305
  async def delete_annotations(image_name: str):
comic_panel_extractor/config.py CHANGED
@@ -1,6 +1,9 @@
1
  from dataclasses import dataclass
2
  import os
3
 
 
 
 
4
  @dataclass
5
  class Config:
6
  """Configuration settings for the comic-to-video pipeline."""
 
1
  from dataclasses import dataclass
2
  import os
3
 
4
+ from dotenv import load_dotenv
5
+ load_dotenv()
6
+
7
  @dataclass
8
  class Config:
9
  """Configuration settings for the comic-to-video pipeline."""
comic_panel_extractor/static/annotator.html CHANGED
@@ -499,6 +499,21 @@
499
  Next →
500
  </button>
501
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
502
 
503
  <div class="file-upload">
504
  <input type="file" id="uploadFile" accept="image/*">
@@ -527,7 +542,7 @@
527
  <div class="section-title">Current Image</div>
528
  <div class="info-card">
529
  <div class="info-row">
530
- <span class="info-label">Boxes</span>
531
  <span class="info-value" id="boxCount">0</span>
532
  </div>
533
  <div class="info-row">
@@ -582,7 +597,7 @@
582
  <div class="canvas-area">
583
  <div class="canvas-toolbar">
584
  <span id="file_name" style="font-size: 13px; color: #4a5568;">
585
- Click and drag to create annotation boxes • Select boxes to move or resize
586
  </span>
587
  </div>
588
 
@@ -604,7 +619,7 @@
604
  constructor() {
605
  this.canvas = document.getElementById('annotationCanvas');
606
  this.ctx = this.canvas.getContext('2d');
607
- this.boxes = [];
608
  this.images = [];
609
  this.currentImageIndex = -1;
610
  this.currentImage = null;
@@ -618,6 +633,12 @@
618
  this.startY = 0;
619
  this.currentBox = null;
620
 
 
 
 
 
 
 
621
  // Box editing state
622
  this.selectedBoxIndex = -1;
623
  this.isDragging = false;
@@ -637,6 +658,16 @@
637
  }
638
 
639
  setupEventListeners() {
 
 
 
 
 
 
 
 
 
 
640
  // Image selection
641
  document.getElementById('imageSelect').addEventListener('change', (e) => {
642
  if (e.target.value) {
@@ -662,7 +693,7 @@
662
  // Action buttons
663
  document.getElementById('saveBtn').addEventListener('click', () => this.saveAnnotations());
664
  document.getElementById('undoBtn').addEventListener('click', () => this.undoLastBox());
665
- document.getElementById('clearBtn').addEventListener('click', () => this.clearAllBoxes());
666
  document.getElementById('reloadBtn').addEventListener('click', () => this.reloadAnnotations());
667
  document.getElementById('detectBtn').addEventListener('click', () => this.detectAnnotations());
668
  document.getElementById('downloadBtn').addEventListener('click', () => this.downloadAnnotations());
@@ -672,6 +703,7 @@
672
  this.canvas.addEventListener('mousemove', (e) => this.onMouseMove(e));
673
  this.canvas.addEventListener('mouseup', () => this.onMouseUp());
674
  this.canvas.addEventListener('mouseleave', () => this.onMouseUp());
 
675
 
676
  // Keyboard events
677
  document.addEventListener('keydown', (e) => this.onKeyDown(e));
@@ -680,6 +712,14 @@
680
  this.canvas.tabIndex = 0;
681
  }
682
 
 
 
 
 
 
 
 
 
683
  async loadImages() {
684
  try {
685
  const response = await fetch('/api/annotate/images');
@@ -757,7 +797,7 @@
757
  this.currentImage = imageName;
758
  this.originalWidth = imageData.width;
759
  this.originalHeight = imageData.height;
760
- this.boxes = annotationsData.boxes || [];
761
  this.selectedBoxIndex = -1;
762
 
763
  // Load and draw image
@@ -779,7 +819,7 @@
779
 
780
  // Update info panel
781
  document.getElementById('currentImageInfo').style.display = 'block';
782
- document.getElementById('boxCount').textContent = this.boxes.length;
783
  document.getElementById('imageSize').textContent = `${imageData.width}×${imageData.height}`;
784
  document.getElementById('selectedBoxInfo').textContent = 'None';
785
 
@@ -793,29 +833,32 @@
793
  drawCanvas() {
794
  this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
795
 
796
- // Draw background image
797
  if (this.backgroundImage) {
798
  this.ctx.drawImage(this.backgroundImage, 0, 0);
799
  }
800
 
801
- // Draw existing boxes
802
- this.boxes.forEach((box, index) => {
803
  let strokeColor = '#00ff00';
804
  let fillColor = 'rgba(0, 255, 0, 0.2)';
805
 
806
  if (index === this.selectedBoxIndex) {
807
  strokeColor = '#0066ff';
808
  fillColor = 'rgba(0, 102, 255, 0.3)';
809
- } else if (!box.saved) {
810
  strokeColor = '#ff0000';
811
  fillColor = 'rgba(255, 0, 0, 0.2)';
812
  }
813
 
814
- this.drawBox(box.left, box.top, box.width, box.height, strokeColor, fillColor);
 
 
 
 
815
 
816
- // Draw resize handles for selected box
817
- if (index === this.selectedBoxIndex) {
818
- this.drawResizeHandles(box);
819
  }
820
  });
821
 
@@ -824,8 +867,173 @@
824
  this.drawBox(this.currentBox.left, this.currentBox.top,
825
  this.currentBox.width, this.currentBox.height, '#ff0000', 'rgba(255, 0, 0, 0.3)');
826
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
827
  }
828
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
829
  drawBox(x, y, width, height, strokeColor, fillColor) {
830
  this.ctx.strokeStyle = strokeColor;
831
  this.ctx.fillStyle = fillColor;
@@ -864,7 +1072,59 @@
864
  this.lastMouseX = pos.x;
865
  this.lastMouseY = pos.y;
866
 
867
- // Check if clicking on a resize handle
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
868
  if (this.selectedBoxIndex >= 0) {
869
  const handle = this.getResizeHandle(pos.x, pos.y);
870
  if (handle) {
@@ -875,7 +1135,6 @@
875
  }
876
  }
877
 
878
- // Check if clicking on an existing box
879
  const clickedBoxIndex = this.getBoxAtPosition(pos.x, pos.y);
880
  if (clickedBoxIndex >= 0) {
881
  this.selectedBoxIndex = clickedBoxIndex;
@@ -888,7 +1147,6 @@
888
  return;
889
  }
890
 
891
- // Start drawing new box
892
  this.selectedBoxIndex = -1;
893
  this.startX = pos.x;
894
  this.startY = pos.y;
@@ -897,11 +1155,73 @@
897
  this.updateSelectedBoxInfo();
898
  }
899
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
900
  onMouseMove(e) {
901
  if (!this.currentImage) return;
902
 
903
  const pos = this.getMousePos(e);
904
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
905
  if (this.isResizing && this.selectedBoxIndex >= 0) {
906
  this.resizeBox(pos.x, pos.y);
907
  this.drawCanvas();
@@ -917,43 +1237,46 @@
917
  this.currentBox.height = pos.y - this.startY;
918
  this.drawCanvas();
919
  } else {
920
- // Update cursor based on what's under mouse
921
  this.updateCursor(pos.x, pos.y);
922
  }
923
  }
924
 
925
  onMouseUp() {
 
 
 
 
 
 
 
 
 
926
  if (this.isDrawing && this.currentBox) {
927
- // Only add box if it has meaningful size
928
  if (Math.abs(this.currentBox.width) > 10 && Math.abs(this.currentBox.height) > 10) {
929
  let left = Math.min(this.startX, this.startX + this.currentBox.width);
930
  let top = Math.min(this.startY, this.startY + this.currentBox.height);
931
  let width = Math.abs(this.currentBox.width);
932
  let height = Math.abs(this.currentBox.height);
933
 
934
- // Apply boundary conditions
935
  left = Math.max(0, left);
936
  top = Math.max(0, top);
937
  width = Math.min(width, this.originalWidth - left);
938
  height = Math.min(height, this.originalHeight - top);
939
 
940
- // Only create box if it still has valid dimensions after boundary check
941
  if (width > 10 && height > 10) {
942
  const box = {
 
943
  left: left,
944
  top: top,
945
  width: width,
946
  height: height,
947
- type: 'rect',
948
- stroke: '#ff0000',
949
- strokeWidth: 3,
950
- fill: 'rgba(255, 0, 0, 0.3)',
951
  saved: false
952
  };
953
 
954
- this.boxes.push(box);
955
- this.selectedBoxIndex = this.boxes.length - 1;
956
- document.getElementById('boxCount').textContent = this.boxes.length;
957
  this.updateSelectedBoxInfo();
958
  }
959
  }
@@ -967,7 +1290,33 @@
967
  this.canvas.style.cursor = 'crosshair';
968
  this.drawCanvas();
969
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
970
 
 
 
 
 
 
 
 
 
 
 
 
 
 
971
 
972
  updateCursor(x, y) {
973
  if (this.selectedBoxIndex >= 0) {
@@ -989,7 +1338,7 @@
989
  getResizeHandle(x, y) {
990
  if (this.selectedBoxIndex < 0) return null;
991
 
992
- const box = this.boxes[this.selectedBoxIndex];
993
  const handleSize = 8;
994
  const tolerance = 5;
995
 
@@ -1013,8 +1362,8 @@
1013
  }
1014
 
1015
  getBoxAtPosition(x, y) {
1016
- for (let i = this.boxes.length - 1; i >= 0; i--) {
1017
- const box = this.boxes[i];
1018
  if (x >= box.left && x <= box.left + box.width &&
1019
  y >= box.top && y <= box.top + box.height) {
1020
  return i;
@@ -1026,7 +1375,7 @@
1026
  resizeBox(x, y) {
1027
  if (this.selectedBoxIndex < 0 || !this.resizeHandle) return;
1028
 
1029
- const box = this.boxes[this.selectedBoxIndex];
1030
  const handle = this.resizeHandle;
1031
 
1032
  switch (handle.type) {
@@ -1119,9 +1468,9 @@
1119
 
1120
 
1121
  moveBox(boxIndex, deltaX, deltaY) {
1122
- if (boxIndex < 0 || boxIndex >= this.boxes.length) return;
1123
 
1124
- const box = this.boxes[boxIndex];
1125
 
1126
  // Calculate new position
1127
  let newLeft = box.left + deltaX;
@@ -1212,7 +1561,7 @@
1212
  resizeSelectedBox(deltaWidth, deltaHeight) {
1213
  if (this.selectedBoxIndex < 0) return;
1214
 
1215
- const box = this.boxes[this.selectedBoxIndex];
1216
 
1217
  // Calculate new dimensions
1218
  let newWidth = box.width + deltaWidth;
@@ -1250,9 +1599,9 @@
1250
 
1251
  deleteSelectedBox() {
1252
  if (this.selectedBoxIndex >= 0) {
1253
- this.boxes.splice(this.selectedBoxIndex, 1);
1254
  this.selectedBoxIndex = -1;
1255
- document.getElementById('boxCount').textContent = this.boxes.length;
1256
  this.updateSelectedBoxInfo();
1257
  this.drawCanvas();
1258
  // this.showAlert('Box deleted', 'info');
@@ -1262,7 +1611,7 @@
1262
  updateSelectedBoxInfo() {
1263
  const selectedBoxInfo = document.getElementById('selectedBoxInfo');
1264
  if (this.selectedBoxIndex >= 0) {
1265
- const box = this.boxes[this.selectedBoxIndex];
1266
  selectedBoxInfo.textContent = `#${this.selectedBoxIndex + 1} (${Math.round(box.left)}, ${Math.round(box.top)}, ${Math.round(box.width)}×${Math.round(box.height)})`;
1267
  } else {
1268
  selectedBoxInfo.textContent = 'None';
@@ -1280,8 +1629,9 @@
1280
  };
1281
  }
1282
 
 
1283
  async saveAnnotations() {
1284
- if (!this.currentImage || this.boxes.length === 0) {
1285
  this.showAlert('No annotations to save', 'info');
1286
  return;
1287
  }
@@ -1293,10 +1643,7 @@
1293
  'Content-Type': 'application/json',
1294
  },
1295
  body: JSON.stringify({
1296
- boxes: this.boxes.map(box => ({
1297
- ...box,
1298
- saved: true
1299
- })),
1300
  image_name: this.currentImage,
1301
  original_width: this.originalWidth,
1302
  original_height: this.originalHeight
@@ -1305,10 +1652,10 @@
1305
 
1306
  if (response.ok) {
1307
  const result = await response.json();
1308
- this.boxes.forEach(box => box.saved = true);
1309
  this.drawCanvas();
1310
  this.showAlert(result.message, 'success');
1311
- this.loadImages(); // Refresh image list to update progress
1312
  } else {
1313
  throw new Error('Failed to save annotations');
1314
  }
@@ -1318,34 +1665,34 @@
1318
  }
1319
 
1320
  undoLastBox() {
1321
- if (this.boxes.length > 0) {
1322
- this.boxes.pop();
1323
  this.selectedBoxIndex = -1;
1324
- document.getElementById('boxCount').textContent = this.boxes.length;
1325
  this.updateSelectedBoxInfo();
1326
  this.drawCanvas();
1327
  this.showAlert('Last box removed', 'info');
1328
  } else {
1329
- this.showAlert('No boxes to undo', 'info');
1330
  }
1331
  }
1332
 
1333
- async clearAllBoxes() {
1334
  if (!this.currentImage) return;
1335
 
1336
- if (confirm('Are you sure you want to clear all boxes and delete the annotation file?')) {
1337
  try {
1338
  // Delete annotations from server
1339
  await fetch(`/api/annotate/annotations/${encodeURIComponent(this.currentImage)}`, {
1340
  method: 'DELETE'
1341
  });
1342
 
1343
- this.boxes = [];
1344
  this.selectedBoxIndex = -1;
1345
  document.getElementById('boxCount').textContent = '0';
1346
  this.updateSelectedBoxInfo();
1347
  this.drawCanvas();
1348
- this.showAlert('All boxes cleared', 'success');
1349
  this.loadImages(); // Refresh progress
1350
  } catch (error) {
1351
  this.showAlert('Error clearing annotations: ' + error.message, 'error');
@@ -1360,12 +1707,12 @@
1360
  const response = await fetch(`/api/annotate/annotations/${encodeURIComponent(this.currentImage)}`);
1361
  const data = await response.json();
1362
 
1363
- this.boxes = (data.boxes || []).map(box => ({
1364
  ...box,
1365
  saved: true
1366
  }));
1367
  this.selectedBoxIndex = -1;
1368
- document.getElementById('boxCount').textContent = this.boxes.length;
1369
  this.updateSelectedBoxInfo();
1370
  this.drawCanvas();
1371
  this.showAlert('Annotations reloaded from file', 'success');
@@ -1381,12 +1728,12 @@
1381
  const response = await fetch(`/api/annotate/detect_annotations/${encodeURIComponent(this.currentImage)}`);
1382
  const data = await response.json();
1383
 
1384
- this.boxes = (data.boxes || []).map(box => ({
1385
  ...box,
1386
  saved: true
1387
  }));
1388
  this.selectedBoxIndex = -1;
1389
- document.getElementById('boxCount').textContent = this.boxes.length;
1390
  this.updateSelectedBoxInfo();
1391
  this.drawCanvas();
1392
  this.showAlert('Annotations detected and loaded from file', 'success');
 
499
  Next →
500
  </button>
501
  </div>
502
+ <!-- Annotation Mode -->
503
+ <div class="sidebar-section">
504
+ <div class="section-title">Annotation Mode</div>
505
+ <div class="form-field">
506
+ <select class="form-select" id="annotationMode">
507
+ <option value="bbox">Bounding Box (YOLO)</option>
508
+ <option value="segmentation">Segmentation (YOLO-seg)</option>
509
+ </select>
510
+ </div>
511
+ <div class="form-field">
512
+ <label class="form-label">Class ID</label>
513
+ <input type="number" class="form-input" id="classId" value="0" min="0" max="100">
514
+ </div>
515
+ </div>
516
+
517
 
518
  <div class="file-upload">
519
  <input type="file" id="uploadFile" accept="image/*">
 
542
  <div class="section-title">Current Image</div>
543
  <div class="info-card">
544
  <div class="info-row">
545
+ <span class="info-label">Annotations</span>
546
  <span class="info-value" id="boxCount">0</span>
547
  </div>
548
  <div class="info-row">
 
597
  <div class="canvas-area">
598
  <div class="canvas-toolbar">
599
  <span id="file_name" style="font-size: 13px; color: #4a5568;">
600
+ Click and drag to create annotation annotations • Select annotations to move or resize
601
  </span>
602
  </div>
603
 
 
619
  constructor() {
620
  this.canvas = document.getElementById('annotationCanvas');
621
  this.ctx = this.canvas.getContext('2d');
622
+ this.annotations = [];
623
  this.images = [];
624
  this.currentImageIndex = -1;
625
  this.currentImage = null;
 
633
  this.startY = 0;
634
  this.currentBox = null;
635
 
636
+ // Segmentation state
637
+ this.annotationMode = 'bbox'; // 'bbox' or 'segmentation'
638
+ this.currentPolygon = null;
639
+ this.isDrawingPolygon = false;
640
+ this.polygonPoints = [];
641
+
642
  // Box editing state
643
  this.selectedBoxIndex = -1;
644
  this.isDragging = false;
 
658
  }
659
 
660
  setupEventListeners() {
661
+ // Mode selection
662
+ document.getElementById('annotationMode').addEventListener('change', (e) => {
663
+ this.annotationMode = e.target.value;
664
+ this.selectedBoxIndex = -1;
665
+ this.polygonPoints = [];
666
+ this.isDrawingPolygon = false;
667
+ this.updateCanvasCursor();
668
+ this.drawCanvas();
669
+ });
670
+
671
  // Image selection
672
  document.getElementById('imageSelect').addEventListener('change', (e) => {
673
  if (e.target.value) {
 
693
  // Action buttons
694
  document.getElementById('saveBtn').addEventListener('click', () => this.saveAnnotations());
695
  document.getElementById('undoBtn').addEventListener('click', () => this.undoLastBox());
696
+ document.getElementById('clearBtn').addEventListener('click', () => this.clearAllAnnotations());
697
  document.getElementById('reloadBtn').addEventListener('click', () => this.reloadAnnotations());
698
  document.getElementById('detectBtn').addEventListener('click', () => this.detectAnnotations());
699
  document.getElementById('downloadBtn').addEventListener('click', () => this.downloadAnnotations());
 
703
  this.canvas.addEventListener('mousemove', (e) => this.onMouseMove(e));
704
  this.canvas.addEventListener('mouseup', () => this.onMouseUp());
705
  this.canvas.addEventListener('mouseleave', () => this.onMouseUp());
706
+ this.canvas.addEventListener('dblclick', (e) => this.onDoubleClick(e));
707
 
708
  // Keyboard events
709
  document.addEventListener('keydown', (e) => this.onKeyDown(e));
 
712
  this.canvas.tabIndex = 0;
713
  }
714
 
715
+ updateCanvasCursor() {
716
+ if (this.annotationMode === 'segmentation') {
717
+ this.canvas.style.cursor = 'crosshair';
718
+ } else {
719
+ this.canvas.style.cursor = 'crosshair';
720
+ }
721
+ }
722
+
723
  async loadImages() {
724
  try {
725
  const response = await fetch('/api/annotate/images');
 
797
  this.currentImage = imageName;
798
  this.originalWidth = imageData.width;
799
  this.originalHeight = imageData.height;
800
+ this.annotations = annotationsData.annotations || [];
801
  this.selectedBoxIndex = -1;
802
 
803
  // Load and draw image
 
819
 
820
  // Update info panel
821
  document.getElementById('currentImageInfo').style.display = 'block';
822
+ document.getElementById('boxCount').textContent = this.annotations.length;
823
  document.getElementById('imageSize').textContent = `${imageData.width}×${imageData.height}`;
824
  document.getElementById('selectedBoxInfo').textContent = 'None';
825
 
 
833
  drawCanvas() {
834
  this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
835
 
 
836
  if (this.backgroundImage) {
837
  this.ctx.drawImage(this.backgroundImage, 0, 0);
838
  }
839
 
840
+ // Draw existing annotations
841
+ this.annotations.forEach((annotation, index) => {
842
  let strokeColor = '#00ff00';
843
  let fillColor = 'rgba(0, 255, 0, 0.2)';
844
 
845
  if (index === this.selectedBoxIndex) {
846
  strokeColor = '#0066ff';
847
  fillColor = 'rgba(0, 102, 255, 0.3)';
848
+ } else if (!annotation.saved) {
849
  strokeColor = '#ff0000';
850
  fillColor = 'rgba(255, 0, 0, 0.2)';
851
  }
852
 
853
+ if (annotation.type === 'segmentation') {
854
+ this.drawPolygon(annotation.points, strokeColor, fillColor);
855
+ } else {
856
+ this.drawBox(annotation.left, annotation.top, annotation.width, annotation.height, strokeColor, fillColor);
857
+ }
858
 
859
+ // Draw resize handles for selected bbox
860
+ if (index === this.selectedBoxIndex && annotation.type === 'bbox') {
861
+ this.drawResizeHandles(annotation);
862
  }
863
  });
864
 
 
867
  this.drawBox(this.currentBox.left, this.currentBox.top,
868
  this.currentBox.width, this.currentBox.height, '#ff0000', 'rgba(255, 0, 0, 0.3)');
869
  }
870
+
871
+ // Draw current polygon being drawn
872
+ if (this.currentPolygon && this.currentPolygon.points.length > 0) {
873
+ this.drawPolygon(this.currentPolygon.points, '#ff0000', 'rgba(255, 0, 0, 0.3)');
874
+ }
875
+ }
876
+ drawPolygon(points, strokeColor, fillColor, isSelected = false) {
877
+ if (points.length < 2) return;
878
+
879
+ this.ctx.strokeStyle = strokeColor;
880
+ this.ctx.fillStyle = fillColor;
881
+ this.ctx.lineWidth = 3;
882
+
883
+ this.ctx.beginPath();
884
+ this.ctx.moveTo(points[0].x, points[0].y);
885
+
886
+ for (let i = 1; i < points.length; i++) {
887
+ this.ctx.lineTo(points[i].x, points[i].y);
888
+ }
889
+
890
+ if (points.length > 2) {
891
+ this.ctx.closePath();
892
+ this.ctx.fill();
893
+ }
894
+ this.ctx.stroke();
895
+
896
+ // Draw points (larger for selected polygon)
897
+ const pointRadius = isSelected ? 6 : 4;
898
+ points.forEach((point, index) => {
899
+ this.ctx.fillStyle = isSelected ? '#0066ff' : strokeColor;
900
+ this.ctx.beginPath();
901
+ this.ctx.arc(point.x, point.y, pointRadius, 0, 2 * Math.PI);
902
+ this.ctx.fill();
903
+
904
+ // Draw point outline for selected polygon
905
+ if (isSelected) {
906
+ this.ctx.strokeStyle = '#ffffff';
907
+ this.ctx.lineWidth = 2;
908
+ this.ctx.stroke();
909
+ }
910
+ });
911
+
912
+ // Draw edge midpoints for selected polygon (for adding points)
913
+ if (isSelected && points.length > 2) {
914
+ this.ctx.fillStyle = 'rgba(0, 102, 255, 0.7)';
915
+ for (let i = 0; i < points.length; i++) {
916
+ const nextIndex = (i + 1) % points.length;
917
+ const midX = (points[i].x + points[nextIndex].x) / 2;
918
+ const midY = (points[i].y + points[nextIndex].y) / 2;
919
+
920
+ this.ctx.beginPath();
921
+ this.ctx.arc(midX, midY, 3, 0, 2 * Math.PI);
922
+ this.ctx.fill();
923
+ }
924
+ }
925
+ }
926
+
927
+ // Polygon-specific editing methods
928
+ getPolygonAtPosition(x, y) {
929
+ for (let i = this.annotations.length - 1; i >= 0; i--) {
930
+ const annotation = this.annotations[i];
931
+ if (annotation.type === 'segmentation' && this.isPointInPolygon(x, y, annotation.points)) {
932
+ return i;
933
+ }
934
+ }
935
+ return -1;
936
  }
937
 
938
+ isPointInPolygon(x, y, points) {
939
+ let inside = false;
940
+ for (let i = 0, j = points.length - 1; i < points.length; j = i++) {
941
+ if (((points[i].y > y) !== (points[j].y > y)) &&
942
+ (x < (points[j].x - points[i].x) * (y - points[i].y) / (points[j].y - points[i].y) + points[i].x)) {
943
+ inside = !inside;
944
+ }
945
+ }
946
+ return inside;
947
+ }
948
+
949
+ getPolygonPointAtPosition(x, y, tolerance = 8) {
950
+ if (this.selectedBoxIndex >= 0 && this.annotations[this.selectedBoxIndex].type === 'segmentation') {
951
+ const points = this.annotations[this.selectedBoxIndex].points;
952
+ for (let i = 0; i < points.length; i++) {
953
+ const distance = Math.sqrt(Math.pow(x - points[i].x, 2) + Math.pow(y - points[i].y, 2));
954
+ if (distance <= tolerance) {
955
+ return i;
956
+ }
957
+ }
958
+ }
959
+ return -1;
960
+ }
961
+
962
+ getPolygonEdgeAtPosition(x, y, tolerance = 5) {
963
+ if (this.selectedBoxIndex >= 0 && this.annotations[this.selectedBoxIndex].type === 'segmentation') {
964
+ const points = this.annotations[this.selectedBoxIndex].points;
965
+ for (let i = 0; i < points.length; i++) {
966
+ const nextIndex = (i + 1) % points.length;
967
+ const p1 = points[i];
968
+ const p2 = points[nextIndex];
969
+
970
+ const distance = this.distanceToLineSegment(x, y, p1.x, p1.y, p2.x, p2.y);
971
+ if (distance <= tolerance) {
972
+ return i; // Return the index of the first point of the edge
973
+ }
974
+ }
975
+ }
976
+ return -1;
977
+ }
978
+
979
+ distanceToLineSegment(px, py, x1, y1, x2, y2) {
980
+ const dx = x2 - x1;
981
+ const dy = y2 - y1;
982
+ const length = Math.sqrt(dx * dx + dy * dy);
983
+
984
+ if (length === 0) return Math.sqrt((px - x1) ** 2 + (py - y1) ** 2);
985
+
986
+ const t = Math.max(0, Math.min(1, ((px - x1) * dx + (py - y1) * dy) / (length * length)));
987
+ const projectionX = x1 + t * dx;
988
+ const projectionY = y1 + t * dy;
989
+
990
+ return Math.sqrt((px - projectionX) ** 2 + (py - projectionY) ** 2);
991
+ }
992
+
993
+ movePolygon(index, deltaX, deltaY) {
994
+ if (index >= 0 && index < this.annotations.length && this.annotations[index].type === 'segmentation') {
995
+ const annotation = this.annotations[index];
996
+ annotation.points = annotation.points.map(point => ({
997
+ x: Math.max(0, Math.min(this.originalWidth, point.x + deltaX)),
998
+ y: Math.max(0, Math.min(this.originalHeight, point.y + deltaY))
999
+ }));
1000
+ annotation.saved = false;
1001
+ }
1002
+ }
1003
+
1004
+ movePolygonPoint(annotationIndex, pointIndex, newX, newY) {
1005
+ if (annotationIndex >= 0 && annotationIndex < this.annotations.length) {
1006
+ const annotation = this.annotations[annotationIndex];
1007
+ if (annotation.type === 'segmentation' && pointIndex >= 0 && pointIndex < annotation.points.length) {
1008
+ annotation.points[pointIndex].x = Math.max(0, Math.min(this.originalWidth, newX));
1009
+ annotation.points[pointIndex].y = Math.max(0, Math.min(this.originalHeight, newY));
1010
+ annotation.saved = false;
1011
+ }
1012
+ }
1013
+ }
1014
+
1015
+ addPolygonPoint(annotationIndex, edgeIndex, x, y) {
1016
+ if (annotationIndex >= 0 && annotationIndex < this.annotations.length) {
1017
+ const annotation = this.annotations[annotationIndex];
1018
+ if (annotation.type === 'segmentation') {
1019
+ const insertIndex = edgeIndex + 1;
1020
+ annotation.points.splice(insertIndex, 0, { x, y });
1021
+ annotation.saved = false;
1022
+ }
1023
+ }
1024
+ }
1025
+
1026
+ removePolygonPoint(annotationIndex, pointIndex) {
1027
+ if (annotationIndex >= 0 && annotationIndex < this.annotations.length) {
1028
+ const annotation = this.annotations[annotationIndex];
1029
+ if (annotation.type === 'segmentation' && annotation.points.length > 3) {
1030
+ annotation.points.splice(pointIndex, 1);
1031
+ annotation.saved = false;
1032
+ }
1033
+ }
1034
+ }
1035
+
1036
+
1037
  drawBox(x, y, width, height, strokeColor, fillColor) {
1038
  this.ctx.strokeStyle = strokeColor;
1039
  this.ctx.fillStyle = fillColor;
 
1072
  this.lastMouseX = pos.x;
1073
  this.lastMouseY = pos.y;
1074
 
1075
+ if (this.annotationMode === 'segmentation') {
1076
+ // Check if clicking on a point of selected polygon
1077
+ if (this.selectedBoxIndex >= 0) {
1078
+ const pointIndex = this.getPolygonPointAtPosition(pos.x, pos.y);
1079
+ if (pointIndex >= 0) {
1080
+ this.isDraggingPoint = true;
1081
+ this.draggingPointIndex = pointIndex;
1082
+ this.canvas.style.cursor = 'move';
1083
+ return;
1084
+ }
1085
+
1086
+ // Check if clicking on an edge to add a point
1087
+ const edgeIndex = this.getPolygonEdgeAtPosition(pos.x, pos.y);
1088
+ if (edgeIndex >= 0 && e.ctrlKey) { // Ctrl+click to add point
1089
+ this.addPolygonPoint(this.selectedBoxIndex, edgeIndex, pos.x, pos.y);
1090
+ this.drawCanvas();
1091
+ return;
1092
+ }
1093
+ }
1094
+
1095
+ // Check if clicking on a polygon
1096
+ const clickedPolygonIndex = this.getPolygonAtPosition(pos.x, pos.y);
1097
+ if (clickedPolygonIndex >= 0) {
1098
+ this.selectedBoxIndex = clickedPolygonIndex;
1099
+ this.isDragging = true;
1100
+ this.dragStartX = pos.x;
1101
+ this.dragStartY = pos.y;
1102
+ this.canvas.style.cursor = 'move';
1103
+ this.updateSelectedBoxInfo();
1104
+ this.drawCanvas();
1105
+ return;
1106
+ }
1107
+
1108
+ // Start new polygon if not drawing one
1109
+ if (!this.isDrawingPolygon) {
1110
+ this.selectedBoxIndex = -1;
1111
+ this.isDrawingPolygon = true;
1112
+ this.polygonPoints = [pos];
1113
+ this.currentPolygon = {
1114
+ points: [...this.polygonPoints],
1115
+ classId: parseInt(document.getElementById('classId').value) || 0,
1116
+ saved: false
1117
+ };
1118
+ } else {
1119
+ // Add point to current polygon
1120
+ this.polygonPoints.push(pos);
1121
+ this.currentPolygon.points = [...this.polygonPoints];
1122
+ }
1123
+ this.drawCanvas();
1124
+ return;
1125
+ }
1126
+
1127
+ // Original bbox logic remains the same...
1128
  if (this.selectedBoxIndex >= 0) {
1129
  const handle = this.getResizeHandle(pos.x, pos.y);
1130
  if (handle) {
 
1135
  }
1136
  }
1137
 
 
1138
  const clickedBoxIndex = this.getBoxAtPosition(pos.x, pos.y);
1139
  if (clickedBoxIndex >= 0) {
1140
  this.selectedBoxIndex = clickedBoxIndex;
 
1147
  return;
1148
  }
1149
 
 
1150
  this.selectedBoxIndex = -1;
1151
  this.startX = pos.x;
1152
  this.startY = pos.y;
 
1155
  this.updateSelectedBoxInfo();
1156
  }
1157
 
1158
+ onDoubleClick(e) {
1159
+ if (this.annotationMode === 'segmentation' && this.isDrawingPolygon && this.polygonPoints.length >= 3) {
1160
+ // Finish polygon
1161
+ this.finishPolygon();
1162
+ }
1163
+ }
1164
+
1165
+ finishPolygon() {
1166
+ if (this.polygonPoints.length >= 3) {
1167
+ const annotation = {
1168
+ type: 'segmentation',
1169
+ points: [...this.polygonPoints],
1170
+ classId: parseInt(document.getElementById('classId').value) || 0,
1171
+ saved: false
1172
+ };
1173
+
1174
+ this.annotations.push(annotation);
1175
+ this.selectedBoxIndex = this.annotations.length - 1;
1176
+ document.getElementById('boxCount').textContent = this.annotations.length;
1177
+ this.updateSelectedBoxInfo();
1178
+ }
1179
+
1180
+ this.isDrawingPolygon = false;
1181
+ this.polygonPoints = [];
1182
+ this.currentPolygon = null;
1183
+ this.drawCanvas();
1184
+ }
1185
+
1186
  onMouseMove(e) {
1187
  if (!this.currentImage) return;
1188
 
1189
  const pos = this.getMousePos(e);
1190
 
1191
+ if (this.annotationMode === 'segmentation') {
1192
+ if (this.isDraggingPoint && this.selectedBoxIndex >= 0) {
1193
+ // Move individual point
1194
+ this.movePolygonPoint(this.selectedBoxIndex, this.draggingPointIndex, pos.x, pos.y);
1195
+ this.drawCanvas();
1196
+ return;
1197
+ }
1198
+
1199
+ if (this.isDragging && this.selectedBoxIndex >= 0) {
1200
+ // Move entire polygon
1201
+ const deltaX = pos.x - this.lastMouseX;
1202
+ const deltaY = pos.y - this.lastMouseY;
1203
+ this.movePolygon(this.selectedBoxIndex, deltaX, deltaY);
1204
+ this.lastMouseX = pos.x;
1205
+ this.lastMouseY = pos.y;
1206
+ this.drawCanvas();
1207
+ return;
1208
+ }
1209
+
1210
+ if (this.isDrawingPolygon) {
1211
+ // Update current polygon preview
1212
+ if (this.currentPolygon) {
1213
+ this.currentPolygon.points = [...this.polygonPoints, pos];
1214
+ this.drawCanvas();
1215
+ }
1216
+ return;
1217
+ }
1218
+
1219
+ // Update cursor based on what's under mouse
1220
+ this.updatePolygonCursor(pos.x, pos.y);
1221
+ return;
1222
+ }
1223
+
1224
+ // Original bbox mouse move logic...
1225
  if (this.isResizing && this.selectedBoxIndex >= 0) {
1226
  this.resizeBox(pos.x, pos.y);
1227
  this.drawCanvas();
 
1237
  this.currentBox.height = pos.y - this.startY;
1238
  this.drawCanvas();
1239
  } else {
 
1240
  this.updateCursor(pos.x, pos.y);
1241
  }
1242
  }
1243
 
1244
  onMouseUp() {
1245
+ if (this.annotationMode === 'segmentation') {
1246
+ this.isDraggingPoint = false;
1247
+ this.draggingPointIndex = -1;
1248
+ this.isDragging = false;
1249
+ this.canvas.style.cursor = 'crosshair';
1250
+ return;
1251
+ }
1252
+
1253
+ // Original bbox mouse up logic...
1254
  if (this.isDrawing && this.currentBox) {
 
1255
  if (Math.abs(this.currentBox.width) > 10 && Math.abs(this.currentBox.height) > 10) {
1256
  let left = Math.min(this.startX, this.startX + this.currentBox.width);
1257
  let top = Math.min(this.startY, this.startY + this.currentBox.height);
1258
  let width = Math.abs(this.currentBox.width);
1259
  let height = Math.abs(this.currentBox.height);
1260
 
 
1261
  left = Math.max(0, left);
1262
  top = Math.max(0, top);
1263
  width = Math.min(width, this.originalWidth - left);
1264
  height = Math.min(height, this.originalHeight - top);
1265
 
 
1266
  if (width > 10 && height > 10) {
1267
  const box = {
1268
+ type: 'bbox',
1269
  left: left,
1270
  top: top,
1271
  width: width,
1272
  height: height,
1273
+ classId: parseInt(document.getElementById('classId').value) || 0,
 
 
 
1274
  saved: false
1275
  };
1276
 
1277
+ this.annotations.push(box);
1278
+ this.selectedBoxIndex = this.annotations.length - 1;
1279
+ document.getElementById('boxCount').textContent = this.annotations.length;
1280
  this.updateSelectedBoxInfo();
1281
  }
1282
  }
 
1290
  this.canvas.style.cursor = 'crosshair';
1291
  this.drawCanvas();
1292
  }
1293
+ updatePolygonCursor(x, y) {
1294
+ if (this.selectedBoxIndex >= 0 && this.annotations[this.selectedBoxIndex].type === 'segmentation') {
1295
+ const pointIndex = this.getPolygonPointAtPosition(x, y);
1296
+ if (pointIndex >= 0) {
1297
+ this.canvas.style.cursor = 'move';
1298
+ return;
1299
+ }
1300
+
1301
+ const edgeIndex = this.getPolygonEdgeAtPosition(x, y);
1302
+ if (edgeIndex >= 0) {
1303
+ this.canvas.style.cursor = 'pointer';
1304
+ return;
1305
+ }
1306
 
1307
+ if (this.isPointInPolygon(x, y, this.annotations[this.selectedBoxIndex].points)) {
1308
+ this.canvas.style.cursor = 'move';
1309
+ return;
1310
+ }
1311
+ }
1312
+
1313
+ const polygonIndex = this.getPolygonAtPosition(x, y);
1314
+ if (polygonIndex >= 0) {
1315
+ this.canvas.style.cursor = 'pointer';
1316
+ } else {
1317
+ this.canvas.style.cursor = 'crosshair';
1318
+ }
1319
+ }
1320
 
1321
  updateCursor(x, y) {
1322
  if (this.selectedBoxIndex >= 0) {
 
1338
  getResizeHandle(x, y) {
1339
  if (this.selectedBoxIndex < 0) return null;
1340
 
1341
+ const box = this.annotations[this.selectedBoxIndex];
1342
  const handleSize = 8;
1343
  const tolerance = 5;
1344
 
 
1362
  }
1363
 
1364
  getBoxAtPosition(x, y) {
1365
+ for (let i = this.annotations.length - 1; i >= 0; i--) {
1366
+ const box = this.annotations[i];
1367
  if (x >= box.left && x <= box.left + box.width &&
1368
  y >= box.top && y <= box.top + box.height) {
1369
  return i;
 
1375
  resizeBox(x, y) {
1376
  if (this.selectedBoxIndex < 0 || !this.resizeHandle) return;
1377
 
1378
+ const box = this.annotations[this.selectedBoxIndex];
1379
  const handle = this.resizeHandle;
1380
 
1381
  switch (handle.type) {
 
1468
 
1469
 
1470
  moveBox(boxIndex, deltaX, deltaY) {
1471
+ if (boxIndex < 0 || boxIndex >= this.annotations.length) return;
1472
 
1473
+ const box = this.annotations[boxIndex];
1474
 
1475
  // Calculate new position
1476
  let newLeft = box.left + deltaX;
 
1561
  resizeSelectedBox(deltaWidth, deltaHeight) {
1562
  if (this.selectedBoxIndex < 0) return;
1563
 
1564
+ const box = this.annotations[this.selectedBoxIndex];
1565
 
1566
  // Calculate new dimensions
1567
  let newWidth = box.width + deltaWidth;
 
1599
 
1600
  deleteSelectedBox() {
1601
  if (this.selectedBoxIndex >= 0) {
1602
+ this.annotations.splice(this.selectedBoxIndex, 1);
1603
  this.selectedBoxIndex = -1;
1604
+ document.getElementById('boxCount').textContent = this.annotations.length;
1605
  this.updateSelectedBoxInfo();
1606
  this.drawCanvas();
1607
  // this.showAlert('Box deleted', 'info');
 
1611
  updateSelectedBoxInfo() {
1612
  const selectedBoxInfo = document.getElementById('selectedBoxInfo');
1613
  if (this.selectedBoxIndex >= 0) {
1614
+ const box = this.annotations[this.selectedBoxIndex];
1615
  selectedBoxInfo.textContent = `#${this.selectedBoxIndex + 1} (${Math.round(box.left)}, ${Math.round(box.top)}, ${Math.round(box.width)}×${Math.round(box.height)})`;
1616
  } else {
1617
  selectedBoxInfo.textContent = 'None';
 
1629
  };
1630
  }
1631
 
1632
+ // Update the save method to handle new format
1633
  async saveAnnotations() {
1634
+ if (!this.currentImage || this.annotations.length === 0) {
1635
  this.showAlert('No annotations to save', 'info');
1636
  return;
1637
  }
 
1643
  'Content-Type': 'application/json',
1644
  },
1645
  body: JSON.stringify({
1646
+ annotations: this.annotations.map(ann => ({ ...ann, saved: true })),
 
 
 
1647
  image_name: this.currentImage,
1648
  original_width: this.originalWidth,
1649
  original_height: this.originalHeight
 
1652
 
1653
  if (response.ok) {
1654
  const result = await response.json();
1655
+ this.annotations.forEach(box => box.saved = true);
1656
  this.drawCanvas();
1657
  this.showAlert(result.message, 'success');
1658
+ this.loadImages();
1659
  } else {
1660
  throw new Error('Failed to save annotations');
1661
  }
 
1665
  }
1666
 
1667
  undoLastBox() {
1668
+ if (this.annotations.length > 0) {
1669
+ this.annotations.pop();
1670
  this.selectedBoxIndex = -1;
1671
+ document.getElementById('boxCount').textContent = this.annotations.length;
1672
  this.updateSelectedBoxInfo();
1673
  this.drawCanvas();
1674
  this.showAlert('Last box removed', 'info');
1675
  } else {
1676
+ this.showAlert('No annotations to undo', 'info');
1677
  }
1678
  }
1679
 
1680
+ async clearAllAnnotations() {
1681
  if (!this.currentImage) return;
1682
 
1683
+ if (confirm('Are you sure you want to clear all annotations and delete the annotation file?')) {
1684
  try {
1685
  // Delete annotations from server
1686
  await fetch(`/api/annotate/annotations/${encodeURIComponent(this.currentImage)}`, {
1687
  method: 'DELETE'
1688
  });
1689
 
1690
+ this.annotations = [];
1691
  this.selectedBoxIndex = -1;
1692
  document.getElementById('boxCount').textContent = '0';
1693
  this.updateSelectedBoxInfo();
1694
  this.drawCanvas();
1695
+ this.showAlert('All annotations cleared', 'success');
1696
  this.loadImages(); // Refresh progress
1697
  } catch (error) {
1698
  this.showAlert('Error clearing annotations: ' + error.message, 'error');
 
1707
  const response = await fetch(`/api/annotate/annotations/${encodeURIComponent(this.currentImage)}`);
1708
  const data = await response.json();
1709
 
1710
+ this.annotations = (data.annotations || []).map(box => ({
1711
  ...box,
1712
  saved: true
1713
  }));
1714
  this.selectedBoxIndex = -1;
1715
+ document.getElementById('boxCount').textContent = this.annotations.length;
1716
  this.updateSelectedBoxInfo();
1717
  this.drawCanvas();
1718
  this.showAlert('Annotations reloaded from file', 'success');
 
1728
  const response = await fetch(`/api/annotate/detect_annotations/${encodeURIComponent(this.currentImage)}`);
1729
  const data = await response.json();
1730
 
1731
+ this.annotations = (data.annotations || []).map(box => ({
1732
  ...box,
1733
  saved: true
1734
  }));
1735
  this.selectedBoxIndex = -1;
1736
+ document.getElementById('boxCount').textContent = this.annotations.length;
1737
  this.updateSelectedBoxInfo();
1738
  this.drawCanvas();
1739
  this.showAlert('Annotations detected and loaded from file', 'success');
comic_panel_extractor/utils.py CHANGED
@@ -7,8 +7,6 @@ import os
7
  import shutil
8
  from glob import glob
9
  from typing import List, Union
10
- from dotenv import load_dotenv
11
- load_dotenv()
12
  from .config import Config
13
 
14
  def remove_duplicate_boxes(boxes, compare_single=None, iou_threshold=0.7):
 
7
  import shutil
8
  from glob import glob
9
  from typing import List, Union
 
 
10
  from .config import Config
11
 
12
  def remove_duplicate_boxes(boxes, compare_single=None, iou_threshold=0.7):
comic_panel_extractor/yolo_manager.py CHANGED
@@ -3,9 +3,9 @@ import os
3
  import shutil
4
  from glob import glob
5
  from typing import List, Union
6
- from dotenv import load_dotenv
7
 
8
- load_dotenv()
 
9
 
10
  def get_abs_path(relative_path: str) -> str:
11
  """Convert relative path to absolute path."""
 
3
  import shutil
4
  from glob import glob
5
  from typing import List, Union
 
6
 
7
+ os.environ["TORCH_USE_CUDA_DSA"] = "1"
8
+ os.environ["CUDA_LAUNCH_BLOCKING"] = "1"
9
 
10
  def get_abs_path(relative_path: str) -> str:
11
  """Convert relative path to absolute path."""