Spaces:
Running
Running
detect added
Browse files
comic_panel_extractor/annorator_server.py
CHANGED
|
@@ -55,11 +55,22 @@ def get_label_path(image_name: str) -> str:
|
|
| 55 |
return os.path.join(LABEL_ROOT, os.path.splitext(image_name)[0] + ".txt")
|
| 56 |
|
| 57 |
# === Core Functions ===
|
| 58 |
-
def load_yolo_boxes(image_path: str, label_path: str):
|
| 59 |
try:
|
| 60 |
img = Image.open(image_path)
|
| 61 |
w, h = img.size
|
| 62 |
boxes = []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
if os.path.exists(label_path):
|
| 64 |
with open(label_path, "r") as f:
|
| 65 |
for line in f:
|
|
@@ -160,6 +171,21 @@ async def get_annotations(image_name: str):
|
|
| 160 |
"original_height": height
|
| 161 |
}
|
| 162 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
@app.post("/api/annotate/annotations")
|
| 164 |
async def save_annotations(request: SaveAnnotationsRequest):
|
| 165 |
label_path = get_label_path(request.image_name)
|
|
|
|
| 55 |
return os.path.join(LABEL_ROOT, os.path.splitext(image_name)[0] + ".txt")
|
| 56 |
|
| 57 |
# === Core Functions ===
|
| 58 |
+
def load_yolo_boxes(image_path: str, label_path: str, detect: bool = False):
|
| 59 |
try:
|
| 60 |
img = Image.open(image_path)
|
| 61 |
w, h = img.size
|
| 62 |
boxes = []
|
| 63 |
+
if detect and not os.path.exists(label_path):
|
| 64 |
+
from .yolo_manager import YOLOManager
|
| 65 |
+
from .utils import Config
|
| 66 |
+
yolo_manager = YOLOManager()
|
| 67 |
+
weights_path = f'{current_path}/{Config.YOLO_MODEL_NAME}.pt'
|
| 68 |
+
|
| 69 |
+
yolo_manager.load_model(weights_path)
|
| 70 |
+
|
| 71 |
+
# Run inference
|
| 72 |
+
_, label_path = yolo_manager.annotate_images(image_paths=[image_path], output_dir=IMAGE_LABEL_ROOT, save_image=False, label_path=label_path)
|
| 73 |
+
|
| 74 |
if os.path.exists(label_path):
|
| 75 |
with open(label_path, "r") as f:
|
| 76 |
for line in f:
|
|
|
|
| 171 |
"original_height": height
|
| 172 |
}
|
| 173 |
|
| 174 |
+
@app.get("/api/annotate/detect_annotations/{image_name:path}")
|
| 175 |
+
async def get_annotations(image_name: str):
|
| 176 |
+
image_path = get_image_path(image_name)
|
| 177 |
+
label_path = get_label_path(image_name)
|
| 178 |
+
|
| 179 |
+
if not os.path.exists(image_path):
|
| 180 |
+
raise HTTPException(status_code=404, detail="Image not found")
|
| 181 |
+
|
| 182 |
+
boxes, (width, height) = load_yolo_boxes(image_path, label_path, True)
|
| 183 |
+
return {
|
| 184 |
+
"boxes": boxes,
|
| 185 |
+
"original_width": width,
|
| 186 |
+
"original_height": height
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
@app.post("/api/annotate/annotations")
|
| 190 |
async def save_annotations(request: SaveAnnotationsRequest):
|
| 191 |
label_path = get_label_path(request.image_name)
|
comic_panel_extractor/static/annotator.html
CHANGED
|
@@ -547,6 +547,9 @@
|
|
| 547 |
<button class="btn btn-primary btn-block" id="reloadBtn">
|
| 548 |
π Reload Annotations
|
| 549 |
</button>
|
|
|
|
|
|
|
|
|
|
| 550 |
<button class="btn btn-secondary btn-block" id="downloadBtn" style="display: none; margin-top: 8px;">
|
| 551 |
π₯ Download
|
| 552 |
</button>
|
|
@@ -661,6 +664,7 @@
|
|
| 661 |
document.getElementById('undoBtn').addEventListener('click', () => this.undoLastBox());
|
| 662 |
document.getElementById('clearBtn').addEventListener('click', () => this.clearAllBoxes());
|
| 663 |
document.getElementById('reloadBtn').addEventListener('click', () => this.reloadAnnotations());
|
|
|
|
| 664 |
document.getElementById('downloadBtn').addEventListener('click', () => this.downloadAnnotations());
|
| 665 |
|
| 666 |
// Canvas events
|
|
@@ -1369,6 +1373,27 @@
|
|
| 1369 |
}
|
| 1370 |
}
|
| 1371 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1372 |
downloadAnnotations() {
|
| 1373 |
if (!this.currentImage) return;
|
| 1374 |
|
|
|
|
| 547 |
<button class="btn btn-primary btn-block" id="reloadBtn">
|
| 548 |
π Reload Annotations
|
| 549 |
</button>
|
| 550 |
+
<button class="btn btn-primary btn-block" id="detectBtn">
|
| 551 |
+
π Detect Annotations
|
| 552 |
+
</button>
|
| 553 |
<button class="btn btn-secondary btn-block" id="downloadBtn" style="display: none; margin-top: 8px;">
|
| 554 |
π₯ Download
|
| 555 |
</button>
|
|
|
|
| 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());
|
| 669 |
|
| 670 |
// Canvas events
|
|
|
|
| 1373 |
}
|
| 1374 |
}
|
| 1375 |
|
| 1376 |
+
async detectAnnotations() {
|
| 1377 |
+
if (!this.currentImage) return;
|
| 1378 |
+
|
| 1379 |
+
try {
|
| 1380 |
+
const response = await fetch(`/api/annotate/detect_annotations/${encodeURIComponent(this.currentImage)}`);
|
| 1381 |
+
const data = await response.json();
|
| 1382 |
+
|
| 1383 |
+
this.boxes = (data.boxes || []).map(box => ({
|
| 1384 |
+
...box,
|
| 1385 |
+
saved: true
|
| 1386 |
+
}));
|
| 1387 |
+
this.selectedBoxIndex = -1;
|
| 1388 |
+
document.getElementById('boxCount').textContent = this.boxes.length;
|
| 1389 |
+
this.updateSelectedBoxInfo();
|
| 1390 |
+
this.drawCanvas();
|
| 1391 |
+
this.showAlert('Annotations reloaded from file', 'success');
|
| 1392 |
+
} catch (error) {
|
| 1393 |
+
this.showAlert('Error reloading annotations: ' + error.message, 'error');
|
| 1394 |
+
}
|
| 1395 |
+
}
|
| 1396 |
+
|
| 1397 |
downloadAnnotations() {
|
| 1398 |
if (!this.currentImage) return;
|
| 1399 |
|
comic_panel_extractor/yolo_manager.py
CHANGED
|
@@ -66,7 +66,7 @@ import os
|
|
| 66 |
import cv2
|
| 67 |
from ultralytics import YOLO
|
| 68 |
from typing import List, Optional, Dict, Any
|
| 69 |
-
from utils import Config, get_abs_path, clean_directory
|
| 70 |
|
| 71 |
class YOLOManager:
|
| 72 |
"""Manages YOLO model training and inference operations."""
|
|
@@ -153,13 +153,13 @@ class YOLOManager:
|
|
| 153 |
|
| 154 |
return weights_path
|
| 155 |
|
| 156 |
-
def annotate_images(self, image_paths: List[str], output_dir: str = 'temp_dir', image_size: int = None) -> None:
|
| 157 |
"""
|
| 158 |
-
Annotate images with model predictions.
|
| 159 |
|
| 160 |
Args:
|
| 161 |
image_paths: List of image file paths
|
| 162 |
-
output_dir: Directory to save annotated images
|
| 163 |
image_size: Size for inference
|
| 164 |
"""
|
| 165 |
if not self.model:
|
|
@@ -169,30 +169,63 @@ class YOLOManager:
|
|
| 169 |
raise ValueError("β No images provided for annotation.")
|
| 170 |
|
| 171 |
image_size = image_size or Config.DEFAULT_IMAGE_SIZE
|
| 172 |
-
clean_directory(output_dir)
|
| 173 |
-
|
| 174 |
-
print(f"π¨ Annotating {
|
| 175 |
|
| 176 |
for idx, image_path in enumerate(image_paths):
|
| 177 |
if not os.path.isfile(image_path):
|
| 178 |
print(f"β οΈ Warning: Skipping non-existent file {image_path}")
|
| 179 |
continue
|
| 180 |
-
|
| 181 |
print(f'π Processing ({idx+1}/{len(image_paths)}): {os.path.basename(image_path)}')
|
| 182 |
|
| 183 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 184 |
results = self.model(image_path, imgsz=image_size)
|
| 185 |
annotated_frame = results[0].plot()
|
| 186 |
|
| 187 |
-
#
|
| 188 |
original_name = os.path.basename(image_path)
|
| 189 |
name, ext = os.path.splitext(original_name)
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 194 |
|
| 195 |
except Exception as e:
|
| 196 |
print(f"β Error processing {image_path}: {str(e)}")
|
| 197 |
-
|
| 198 |
-
print(f"π Annotation complete! Results saved to: {output_dir}")
|
|
|
|
| 66 |
import cv2
|
| 67 |
from ultralytics import YOLO
|
| 68 |
from typing import List, Optional, Dict, Any
|
| 69 |
+
from .utils import Config, get_abs_path, clean_directory
|
| 70 |
|
| 71 |
class YOLOManager:
|
| 72 |
"""Manages YOLO model training and inference operations."""
|
|
|
|
| 153 |
|
| 154 |
return weights_path
|
| 155 |
|
| 156 |
+
def annotate_images(self, image_paths: List[str], output_dir: str = 'temp_dir', image_size: int = None, save_image: bool = True, label_path: str = None) -> None:
|
| 157 |
"""
|
| 158 |
+
Annotate images with model predictions and save YOLO-format label files.
|
| 159 |
|
| 160 |
Args:
|
| 161 |
image_paths: List of image file paths
|
| 162 |
+
output_dir: Directory to save annotated images and labels
|
| 163 |
image_size: Size for inference
|
| 164 |
"""
|
| 165 |
if not self.model:
|
|
|
|
| 169 |
raise ValueError("β No images provided for annotation.")
|
| 170 |
|
| 171 |
image_size = image_size or Config.DEFAULT_IMAGE_SIZE
|
| 172 |
+
# clean_directory(output_dir)
|
| 173 |
+
totla_images = len(image_paths)
|
| 174 |
+
print(f"π¨ Annotating {totla_images} images and saving labels...")
|
| 175 |
|
| 176 |
for idx, image_path in enumerate(image_paths):
|
| 177 |
if not os.path.isfile(image_path):
|
| 178 |
print(f"β οΈ Warning: Skipping non-existent file {image_path}")
|
| 179 |
continue
|
| 180 |
+
|
| 181 |
print(f'π Processing ({idx+1}/{len(image_paths)}): {os.path.basename(image_path)}')
|
| 182 |
|
| 183 |
try:
|
| 184 |
+
# Load image for size info
|
| 185 |
+
img = cv2.imread(image_path)
|
| 186 |
+
h, w = img.shape[:2]
|
| 187 |
+
|
| 188 |
+
# Run inference
|
| 189 |
results = self.model(image_path, imgsz=image_size)
|
| 190 |
annotated_frame = results[0].plot()
|
| 191 |
|
| 192 |
+
# Prepare save paths
|
| 193 |
original_name = os.path.basename(image_path)
|
| 194 |
name, ext = os.path.splitext(original_name)
|
| 195 |
+
|
| 196 |
+
save_img_path = None
|
| 197 |
+
save_txt_path = os.path.join(output_dir, f'{name}.txt') # YOLO label txt
|
| 198 |
+
if save_image:
|
| 199 |
+
save_img_path = os.path.join(output_dir, f'annotated_{name}{ext}')
|
| 200 |
+
# Save annotated image
|
| 201 |
+
cv2.imwrite(save_img_path, annotated_frame)
|
| 202 |
+
|
| 203 |
+
# Write YOLO label file
|
| 204 |
+
with open(save_txt_path, 'w') as f:
|
| 205 |
+
for box in results[0].boxes:
|
| 206 |
+
# box.xyxy format: (xmin, ymin, xmax, ymax)
|
| 207 |
+
xyxy = box.xyxy[0].tolist()
|
| 208 |
+
cls_id = int(box.cls[0].item()) # class id
|
| 209 |
+
|
| 210 |
+
xmin, ymin, xmax, ymax = xyxy
|
| 211 |
+
# Convert to YOLO format (normalized)
|
| 212 |
+
x_center = ((xmin + xmax) / 2) / w
|
| 213 |
+
y_center = ((ymin + ymax) / 2) / h
|
| 214 |
+
width = (xmax - xmin) / w
|
| 215 |
+
height = (ymax - ymin) / h
|
| 216 |
+
|
| 217 |
+
# Write one line per object
|
| 218 |
+
f.write(f"{cls_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}\n")
|
| 219 |
+
|
| 220 |
+
if label_path:
|
| 221 |
+
shutil.copyfile(save_txt_path, label_path)
|
| 222 |
+
print(f'β
Saved annotated image: {save_img_path}')
|
| 223 |
+
print(f'β
Saved label file: {save_txt_path}')
|
| 224 |
+
print(f"π Annotation and label saving complete! Results saved to: {output_dir}")
|
| 225 |
+
|
| 226 |
+
if totla_images == 1:
|
| 227 |
+
return save_img_path, save_txt_path
|
| 228 |
|
| 229 |
except Exception as e:
|
| 230 |
print(f"β Error processing {image_path}: {str(e)}")
|
| 231 |
+
return None, None
|
|
|