trying to improve annotation
This commit is contained in:
@ -80,9 +80,13 @@ CANVAS_GLOBAL_JS = r"""
|
||||
boxes = [];
|
||||
}
|
||||
|
||||
const hiddenInput = document.getElementById('canvas-boxes-data');
|
||||
const hiddenRoot = document.getElementById('canvas-boxes-data');
|
||||
const hiddenInput = hiddenRoot
|
||||
? (hiddenRoot.querySelector('textarea, input') || hiddenRoot)
|
||||
: null;
|
||||
const syncHidden = () => {
|
||||
if (!hiddenInput) return;
|
||||
if (!('value' in hiddenInput)) return;
|
||||
hiddenInput.value = JSON.stringify(boxes);
|
||||
hiddenInput.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
};
|
||||
@ -233,16 +237,23 @@ CANVAS_GLOBAL_JS = r"""
|
||||
const y1 = Math.min(createStart.y, dragStart.y);
|
||||
const x2 = Math.max(createStart.x, dragStart.x);
|
||||
const y2 = Math.max(createStart.y, dragStart.y);
|
||||
if (x2 - x1 > 10 && y2 - y1 > 10) {
|
||||
boxes.push({
|
||||
bbox: [x1, y1, x2, y2],
|
||||
label: 'knot',
|
||||
confidence: 1.0,
|
||||
source: 'manual',
|
||||
});
|
||||
syncHidden();
|
||||
redraw();
|
||||
const w = x2 - x1;
|
||||
const h = y2 - y1;
|
||||
if (w > 10 && h > 10) {
|
||||
boxes.push({ bbox: [x1, y1, x2, y2], label: 'knot', confidence: 1.0, source: 'manual' });
|
||||
} else {
|
||||
// Click without drag: create a default-size box around the click.
|
||||
const size = 120;
|
||||
const cx = createStart.x;
|
||||
const cy = createStart.y;
|
||||
const bx1 = Math.max(0, cx - size / 2);
|
||||
const by1 = Math.max(0, cy - size / 2);
|
||||
const bx2 = Math.min(displayWidth, cx + size / 2);
|
||||
const by2 = Math.min(displayHeight, cy + size / 2);
|
||||
boxes.push({ bbox: [bx1, by1, bx2, by2], label: 'knot', confidence: 1.0, source: 'manual' });
|
||||
}
|
||||
syncHidden();
|
||||
redraw();
|
||||
}
|
||||
|
||||
isDragging = false;
|
||||
@ -264,9 +275,13 @@ CANVAS_GLOBAL_JS = r"""
|
||||
const obs = new MutationObserver(() => scan());
|
||||
obs.observe(document.documentElement, { childList: true, subtree: true });
|
||||
window.addEventListener('load', () => scan());
|
||||
setTimeout(() => scan(), 0);
|
||||
setTimeout(() => scan(), 250);
|
||||
setTimeout(() => scan(), 1000);
|
||||
scan();
|
||||
setTimeout(() => scan(), 0);
|
||||
setTimeout(() => scan(), 100);
|
||||
setTimeout(() => scan(), 250);
|
||||
setTimeout(() => scan(), 500);
|
||||
setTimeout(() => scan(), 1000);
|
||||
setTimeout(() => scan(), 2000);
|
||||
})();
|
||||
"""
|
||||
|
||||
@ -536,7 +551,7 @@ class AnnotationApp:
|
||||
# Format as readable list
|
||||
lines = ["📂 Available Models:"]
|
||||
for i, model in enumerate(available_models, 1):
|
||||
lines.append(f"{i}. {model['dir']} → {model['path'].name} ({model['type'].upper()})")
|
||||
lines.append(f"{i}. {model['dir']} -> {model['path'].name} ({model['type'].upper()})")
|
||||
|
||||
lines.append("\n💡 Use the Model Selector dropdown above to quickly switch models")
|
||||
return "\n".join(lines)
|
||||
@ -562,14 +577,14 @@ class AnnotationApp:
|
||||
|
||||
return f"❌ Model '{model_display}' not found"
|
||||
|
||||
def load_new_images_dir(self, images_dir: str) -> tuple[str, str, str]:
|
||||
def load_new_images_dir(self, images_dir: str) -> tuple[str, str, str, str]:
|
||||
"""Load a new images directory from the GUI."""
|
||||
path = Path(images_dir)
|
||||
if not path.exists():
|
||||
return "<div>Directory not found</div>", "", f"❌ Directory not found: {images_dir}"
|
||||
return "<div>Directory not found</div>", "", "Image: -/-", f"❌ Directory not found: {images_dir}"
|
||||
|
||||
if not path.is_dir():
|
||||
return "<div>Not a directory</div>", "", f"❌ Not a directory: {images_dir}"
|
||||
return "<div>Not a directory</div>", "", "Image: -/-", f"❌ Not a directory: {images_dir}"
|
||||
|
||||
result = self._load_images(path)
|
||||
|
||||
@ -578,11 +593,12 @@ class AnnotationApp:
|
||||
img, filename = self.get_current_image()
|
||||
boxes = self.annotations.get(filename, [])
|
||||
img_html = self.generate_interactive_canvas(boxes)
|
||||
boxes_text = self._format_boxes_text(boxes)
|
||||
info = f"{result}\nImage 1/{len(self.image_paths)}: {filename}"
|
||||
return img_html, boxes_text, info
|
||||
boxes_html = self._format_boxes_html(boxes)
|
||||
image_label = self._current_image_label(filename)
|
||||
status = result
|
||||
return img_html, boxes_html, image_label, status
|
||||
else:
|
||||
return "<div>No images found</div>", "", f"{result}\n⚠️ No .jpg or .png images found in directory"
|
||||
return "<div>No images found</div>", "", "Image: -/-", f"{result}\n⚠️ No .jpg or .png images found in directory"
|
||||
|
||||
def get_current_model_info(self) -> str:
|
||||
"""Get info about currently loaded model."""
|
||||
@ -597,6 +613,12 @@ class AnnotationApp:
|
||||
def get_current_dir_info(self) -> str:
|
||||
"""Get info about current images directory."""
|
||||
return f"📁 {self.images_dir} ({len(self.image_paths)} images)"
|
||||
|
||||
def _current_image_label(self, filename: str) -> str:
|
||||
"""Stable image index display (kept separate from status messages)."""
|
||||
if not filename or not self.image_paths:
|
||||
return "Image: -/-"
|
||||
return f"Image {self.current_idx + 1}/{len(self.image_paths)}: {filename}"
|
||||
|
||||
def get_current_image(self) -> tuple[Image.Image, str]:
|
||||
"""Get current image and filename."""
|
||||
@ -671,30 +693,64 @@ class AnnotationApp:
|
||||
<textarea id="annotation-initial-boxes" style="display:none;">{boxes_escaped}</textarea>
|
||||
<div data-annotation-root="1" style="position: relative; width: {display_width}px; height: {display_height}px;">
|
||||
<img id="annotation-img" src="data:image/png;base64,{img_base64}"
|
||||
style="position:absolute; left:0; top:0; width:{display_width}px; height:{display_height}px;" />
|
||||
style="position:absolute; left:0; top:0; width:{display_width}px; height:{display_height}px; z-index: 1;" />
|
||||
<canvas id="annotation-canvas" width="{display_width}" height="{display_height}"
|
||||
style="position:absolute; left:0; top:0; width:{display_width}px; height:{display_height}px; cursor: crosshair; background: transparent;"></canvas>
|
||||
style="position:absolute; left:0; top:0; width:{display_width}px; height:{display_height}px; cursor: crosshair; background: transparent; z-index: 2; pointer-events: auto;"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
return html_out
|
||||
|
||||
def _format_boxes_text(self, boxes: list[dict]) -> str:
|
||||
"""Format boxes for display."""
|
||||
def _format_boxes_html(self, boxes: list[dict]) -> str:
|
||||
"""Format boxes as HTML list with delete buttons."""
|
||||
if not boxes:
|
||||
return "No annotations"
|
||||
return "<div style='color: #999; font-style: italic;'>No annotations</div>"
|
||||
|
||||
lines = []
|
||||
lines = ["<div style='font-family: monospace; font-size: 13px;'>"]
|
||||
for i, box in enumerate(boxes):
|
||||
x1, y1, x2, y2 = box["bbox"]
|
||||
conf = box.get("confidence", 1.0)
|
||||
source = box.get("source", "manual")
|
||||
lines.append(f"{i}: [{x1:.0f}, {y1:.0f}, {x2:.0f}, {y2:.0f}] conf={conf:.2f} ({source})")
|
||||
|
||||
return "\n".join(lines)
|
||||
lines.append(
|
||||
f"<div style='margin: 4px 0; padding: 4px; background: #f5f5f5; border-radius: 3px; display: flex; justify-content: space-between; align-items: center;'>"
|
||||
f"<span style='flex: 1;'>{i}: [{x1:.0f},{y1:.0f},{x2:.0f},{y2:.0f}] {conf:.2f} ({source})</span>"
|
||||
f"<button onclick='window.deleteBox({i})' style='background: #ff4444; color: white; border: none; padding: 2px 8px; border-radius: 3px; cursor: pointer; margin-left: 8px;'>✕</button>"
|
||||
f"</div>"
|
||||
)
|
||||
lines.append("</div>")
|
||||
return "".join(lines)
|
||||
|
||||
def _parse_boxes_text(self, text: str) -> list[dict] | None:
|
||||
"""Parse edited JSON from the Annotations textbox."""
|
||||
if not text:
|
||||
return None
|
||||
try:
|
||||
data = json.loads(text)
|
||||
except json.JSONDecodeError:
|
||||
return None
|
||||
if not isinstance(data, list):
|
||||
return None
|
||||
cleaned: list[dict] = []
|
||||
for item in data:
|
||||
if not isinstance(item, dict) or "bbox" not in item:
|
||||
continue
|
||||
bbox = item.get("bbox")
|
||||
if not (isinstance(bbox, list) and len(bbox) == 4):
|
||||
continue
|
||||
try:
|
||||
x1, y1, x2, y2 = [float(v) for v in bbox]
|
||||
except Exception:
|
||||
continue
|
||||
cleaned.append({
|
||||
"bbox": [x1, y1, x2, y2],
|
||||
"label": item.get("label", "knot"),
|
||||
"confidence": float(item.get("confidence", 1.0)),
|
||||
"source": item.get("source", "manual"),
|
||||
})
|
||||
return cleaned
|
||||
|
||||
def load_image(self, direction: str = "current") -> tuple[str, str, str]:
|
||||
def load_image(self, direction: str = "current") -> tuple[str, str, str, str]:
|
||||
"""Load image (current/next/prev)."""
|
||||
if direction == "next":
|
||||
self.current_idx = min(self.current_idx + 1, len(self.image_paths) - 1)
|
||||
@ -703,21 +759,21 @@ class AnnotationApp:
|
||||
|
||||
img, filename = self.get_current_image()
|
||||
if not img:
|
||||
return "<div>No images</div>", "", "No images"
|
||||
return "<div>No images</div>", "", "Image: -/-", "No images"
|
||||
|
||||
# Load existing annotations
|
||||
boxes = self.annotations.get(filename, [])
|
||||
img_html = self.generate_interactive_canvas(boxes)
|
||||
boxes_text = self._format_boxes_text(boxes)
|
||||
info = f"Image {self.current_idx + 1}/{len(self.image_paths)}: {filename}"
|
||||
|
||||
return img_html, boxes_text, info
|
||||
boxes_html = self._format_boxes_html(boxes)
|
||||
image_label = self._current_image_label(filename)
|
||||
|
||||
return img_html, boxes_html, image_label, ""
|
||||
|
||||
def add_box_manual(self, x1: int, y1: int, x2: int, y2: int) -> tuple[str, str, str]:
|
||||
def add_box_manual(self, x1: int, y1: int, x2: int, y2: int) -> tuple[str, str, str, str]:
|
||||
"""Manually add a bounding box."""
|
||||
img, filename = self.get_current_image()
|
||||
if not img:
|
||||
return "<div>No images</div>", "", "No images"
|
||||
return "<div>No images</div>", "", "Image: -/-", "No images"
|
||||
|
||||
# Add box
|
||||
box = {
|
||||
@ -735,16 +791,14 @@ class AnnotationApp:
|
||||
# Redraw
|
||||
boxes = self.annotations[filename]
|
||||
img_html = self.generate_interactive_canvas(boxes)
|
||||
boxes_text = self._format_boxes_text(boxes)
|
||||
info = f"✓ Added box: {len(boxes)} total | Image {self.current_idx + 1}/{len(self.image_paths)}: {filename}"
|
||||
|
||||
return img_html, boxes_text, info
|
||||
boxes_html = self._format_boxes_html(boxes)
|
||||
return img_html, boxes_html, self._current_image_label(filename), f"✓ Added box: {len(boxes)} total"
|
||||
|
||||
def delete_last_box(self) -> tuple[str, str, str]:
|
||||
def delete_last_box(self) -> tuple[str, str, str, str]:
|
||||
"""Delete the last box from current image."""
|
||||
img, filename = self.get_current_image()
|
||||
if not img:
|
||||
return "<div>No images</div>", "", "No images"
|
||||
return "<div>No images</div>", "", "Image: -/-", "No images"
|
||||
|
||||
if filename in self.annotations and self.annotations[filename]:
|
||||
self.annotations[filename].pop()
|
||||
@ -753,35 +807,50 @@ class AnnotationApp:
|
||||
# Redraw
|
||||
boxes = self.annotations.get(filename, [])
|
||||
img_html = self.generate_interactive_canvas(boxes)
|
||||
boxes_text = self._format_boxes_text(boxes)
|
||||
info = f"✓ Deleted last box: {len(boxes)} remaining | Image {self.current_idx + 1}/{len(self.image_paths)}: {filename}"
|
||||
|
||||
return img_html, boxes_text, info
|
||||
boxes_html = self._format_boxes_html(boxes)
|
||||
return img_html, boxes_html, self._current_image_label(filename), f"✓ Deleted last box: {len(boxes)} remaining"
|
||||
|
||||
def clear_boxes(self) -> tuple[str, str, str]:
|
||||
def delete_box_by_index(self, index: int) -> tuple[str, str, str, str]:
|
||||
"""Delete a specific box by index."""
|
||||
img, filename = self.get_current_image()
|
||||
if not img:
|
||||
return "<div>No images</div>", "", "Image: -/-", "No images"
|
||||
|
||||
boxes = self.annotations.get(filename, [])
|
||||
if 0 <= index < len(boxes):
|
||||
boxes.pop(index)
|
||||
self.annotations[filename] = boxes
|
||||
self._save_annotations()
|
||||
img_html = self.generate_interactive_canvas(boxes)
|
||||
boxes_html = self._format_boxes_html(boxes)
|
||||
return img_html, boxes_html, self._current_image_label(filename), f"✓ Deleted box {index}"
|
||||
else:
|
||||
img_html = self.generate_interactive_canvas(boxes)
|
||||
boxes_html = self._format_boxes_html(boxes)
|
||||
return img_html, boxes_html, self._current_image_label(filename), "❌ Invalid box index"
|
||||
|
||||
def clear_boxes(self) -> tuple[str, str, str, str]:
|
||||
"""Clear all boxes from current image."""
|
||||
img, filename = self.get_current_image()
|
||||
if not img:
|
||||
return "<div>No images</div>", "", "No images"
|
||||
return "<div>No images</div>", "", "Image: -/-", "No images"
|
||||
|
||||
self.annotations[filename] = []
|
||||
self._save_annotations()
|
||||
|
||||
boxes = []
|
||||
img_html = self.generate_interactive_canvas(boxes)
|
||||
boxes_text = "No annotations"
|
||||
info = f"✓ Cleared all boxes | Image {self.current_idx + 1}/{len(self.image_paths)}: {filename}"
|
||||
|
||||
return img_html, boxes_text, info
|
||||
boxes_html = self._format_boxes_html(boxes)
|
||||
return img_html, boxes_html, self._current_image_label(filename), "✓ Cleared all boxes"
|
||||
|
||||
def auto_label_current(self, threshold: float = 0.5) -> tuple[str, str, str]:
|
||||
def auto_label_current(self, threshold: float = 0.5) -> tuple[str, str, str, str]:
|
||||
"""Auto-label current image using loaded model."""
|
||||
if not self.model:
|
||||
return "<div>No model loaded</div>", "", "❌ No model loaded"
|
||||
return "<div>No model loaded</div>", "", "Image: -/-", "❌ No model loaded"
|
||||
|
||||
img, filename = self.get_current_image()
|
||||
if not img:
|
||||
return "<div>No images</div>", "", "No images"
|
||||
return "<div>No images</div>", "", "Image: -/-", "No images"
|
||||
|
||||
try:
|
||||
# Run inference based on model type
|
||||
@ -828,40 +897,37 @@ class AnnotationApp:
|
||||
|
||||
# Redraw
|
||||
img_html = self.generate_interactive_canvas(self.annotations[filename])
|
||||
boxes_text = self._format_boxes_text(self.annotations[filename])
|
||||
info = f"🤖 Auto-labeled: {len(boxes)} detections | Image {self.current_idx + 1}/{len(self.image_paths)}: {filename}"
|
||||
|
||||
return img_html, boxes_text, info
|
||||
boxes_html = self._format_boxes_html(self.annotations[filename])
|
||||
return img_html, boxes_html, self._current_image_label(filename), f"🤖 Auto-labeled: {len(boxes)} detections"
|
||||
|
||||
except Exception as e:
|
||||
boxes = self.annotations.get(filename, [])
|
||||
img_html = self.generate_interactive_canvas(boxes)
|
||||
return img_html, self._format_boxes_text(boxes), f"❌ Auto-label failed: {e}"
|
||||
return img_html, self._format_boxes_html(boxes), self._current_image_label(filename), f"❌ Auto-label failed: {e}"
|
||||
|
||||
def save_canvas_changes(self, boxes_json: str) -> tuple[str, str, str]:
|
||||
"""Save changes made in the interactive canvas."""
|
||||
def save_canvas_changes(self, boxes_json: str) -> tuple[str, str, str, str]:
|
||||
"""Auto-save changes made in the canvas."""
|
||||
img, filename = self.get_current_image()
|
||||
if not img:
|
||||
return "<div>No images</div>", "", "No images"
|
||||
|
||||
return "<div>No images</div>", "", "Image: -/-", "No images"
|
||||
|
||||
try:
|
||||
# Parse the boxes from JSON
|
||||
if boxes_json:
|
||||
boxes = json.loads(boxes_json)
|
||||
self.annotations[filename] = boxes
|
||||
self._save_annotations()
|
||||
info = f"✓ Saved {len(boxes)} boxes | Image {self.current_idx + 1}/{len(self.image_paths)}: {filename}"
|
||||
status = ""
|
||||
else:
|
||||
boxes = self.annotations.get(filename, [])
|
||||
info = f"✓ No changes to save | Image {self.current_idx + 1}/{len(self.image_paths)}: {filename}"
|
||||
status = ""
|
||||
except json.JSONDecodeError:
|
||||
boxes = self.annotations.get(filename, [])
|
||||
info = f"❌ Invalid boxes data | Image {self.current_idx + 1}/{len(self.image_paths)}: {filename}"
|
||||
|
||||
status = "❌ Invalid canvas data"
|
||||
|
||||
img_html = self.generate_interactive_canvas(boxes)
|
||||
boxes_text = self._format_boxes_text(boxes)
|
||||
|
||||
return img_html, boxes_text, info
|
||||
boxes_html = self._format_boxes_html(boxes)
|
||||
|
||||
return img_html, boxes_html, self._current_image_label(filename), status
|
||||
|
||||
def _save_annotations(self):
|
||||
"""Save annotations to JSON file."""
|
||||
@ -1089,7 +1155,7 @@ class AnnotationApp:
|
||||
self.training_process.wait()
|
||||
|
||||
if self.training_process.returncode == 0:
|
||||
self.training_status = "✅ Training completed successfully!"
|
||||
self.training_status = "[OK] Training completed successfully!"
|
||||
# Reload model with new weights
|
||||
if framework == "RF-DETR":
|
||||
# RF-DETR uses checkpoint_best_total.pth
|
||||
@ -1229,7 +1295,7 @@ class AnnotationApp:
|
||||
return (
|
||||
f"✓ {model_type.upper()} exported to ONNX!\n"
|
||||
f"📁 Output: {output_path}\n"
|
||||
f"🔗 Next: Convert ONNX → RVC using HubAI (online) or ModelConverter (offline).\n"
|
||||
f"Next: Convert ONNX -> RVC using HubAI (online) or ModelConverter (offline).\n"
|
||||
f"Docs: https://docs.luxonis.com/software-v3/ai-inference/conversion/\n"
|
||||
f"💡 Offline conversion: Use Luxonis ModelConverter with Docker\n"
|
||||
f"⚠️ OpenVINO export not available: {str(e)}"
|
||||
@ -1245,8 +1311,8 @@ def create_ui(app: AnnotationApp) -> gr.Blocks:
|
||||
|
||||
with gr.Blocks(title="Knot Annotation Tool") as demo:
|
||||
gr.Markdown("""
|
||||
# 🪵 Wood Knot Annotation Tool
|
||||
**Label → Train → Auto-Label → Repeat**
|
||||
# Wood Knot Annotation Tool
|
||||
**Label -> Train -> Auto-Label -> Repeat**
|
||||
|
||||
- Manually annotate images or use **Auto-Label** with your trained model
|
||||
- Export and prepare dataset for training
|
||||
@ -1256,7 +1322,7 @@ def create_ui(app: AnnotationApp) -> gr.Blocks:
|
||||
""")
|
||||
|
||||
# Settings section at the top
|
||||
with gr.Accordion("⚙️ Settings", open=False):
|
||||
with gr.Accordion("Settings", open=False):
|
||||
with gr.Row():
|
||||
with gr.Column():
|
||||
images_dir_input = gr.Textbox(
|
||||
@ -1307,7 +1373,6 @@ def create_ui(app: AnnotationApp) -> gr.Blocks:
|
||||
prev_btn = gr.Button("⬅️ Previous")
|
||||
next_btn = gr.Button("Next ➡️")
|
||||
auto_label_btn = gr.Button("🤖 Auto-Label", variant="primary")
|
||||
save_canvas_btn = gr.Button("💾 Save Canvas Changes")
|
||||
|
||||
# Hidden textbox to store canvas boxes data
|
||||
canvas_boxes_data = gr.Textbox(visible=False, elem_id="canvas-boxes-data")
|
||||
@ -1316,8 +1381,10 @@ def create_ui(app: AnnotationApp) -> gr.Blocks:
|
||||
threshold_slider = gr.Slider(0.1, 0.9, DEFAULT_DETECTION_THRESHOLD, label="Detection Threshold")
|
||||
|
||||
with gr.Column(scale=1):
|
||||
image_index_text = gr.Textbox(label="Image", lines=1, interactive=False)
|
||||
info_text = gr.Textbox(label="Status", lines=2)
|
||||
boxes_text = gr.Textbox(label="Annotations", lines=10)
|
||||
boxes_html = gr.HTML(label="Annotations")
|
||||
delete_box_index = gr.Number(visible=False, value=-1)
|
||||
|
||||
gr.Markdown("### Manual Annotation")
|
||||
with gr.Row():
|
||||
@ -1336,11 +1403,11 @@ def create_ui(app: AnnotationApp) -> gr.Blocks:
|
||||
label="Export Path",
|
||||
value="annotations_coco.json"
|
||||
)
|
||||
export_btn = gr.Button("💾 Export COCO")
|
||||
export_btn = gr.Button("Export COCO")
|
||||
export_result = gr.Textbox(label="Export Result", lines=1)
|
||||
|
||||
# Training tab
|
||||
with gr.Tab("🎯 Training"):
|
||||
with gr.Tab("Training"):
|
||||
gr.Markdown("""
|
||||
### Train Object Detection Model
|
||||
|
||||
@ -1350,7 +1417,7 @@ def create_ui(app: AnnotationApp) -> gr.Blocks:
|
||||
- **YOLOv6** (MIT): Fast, proven on OAK cameras
|
||||
- **YOLOX** (MIT): Similar to YOLOv6, slight differences
|
||||
|
||||
**All MIT/Apache 2.0 licensed - free for commercial use!** ✅
|
||||
**All MIT/Apache 2.0 licensed - free for commercial use!**
|
||||
|
||||
**Steps:**
|
||||
1. Annotate at least 50-100 images in the Annotation tab
|
||||
@ -1492,7 +1559,7 @@ def create_ui(app: AnnotationApp) -> gr.Blocks:
|
||||
load_images_btn.click(
|
||||
app.load_new_images_dir,
|
||||
inputs=[images_dir_input],
|
||||
outputs=[image_display, boxes_text, info_text]
|
||||
outputs=[image_display, boxes_html, image_index_text, info_text]
|
||||
).then(
|
||||
lambda: (app.get_current_dir_info(), app.get_current_model_info()),
|
||||
outputs=[dir_info, model_info]
|
||||
@ -1526,47 +1593,48 @@ def create_ui(app: AnnotationApp) -> gr.Blocks:
|
||||
|
||||
prev_btn.click(
|
||||
lambda: app.load_image("prev"),
|
||||
outputs=[image_display, boxes_text, info_text]
|
||||
outputs=[image_display, boxes_html, image_index_text, info_text]
|
||||
)
|
||||
|
||||
next_btn.click(
|
||||
lambda: app.load_image("next"),
|
||||
outputs=[image_display, boxes_text, info_text]
|
||||
outputs=[image_display, boxes_html, image_index_text, info_text]
|
||||
)
|
||||
|
||||
auto_label_btn.click(
|
||||
lambda t: app.auto_label_current(t),
|
||||
inputs=[threshold_slider],
|
||||
outputs=[image_display, boxes_text, info_text]
|
||||
outputs=[image_display, boxes_html, image_index_text, info_text]
|
||||
)
|
||||
|
||||
save_canvas_btn.click(
|
||||
# Auto-save when canvas changes
|
||||
canvas_boxes_data.change(
|
||||
app.save_canvas_changes,
|
||||
inputs=[canvas_boxes_data],
|
||||
outputs=[image_display, boxes_text, info_text],
|
||||
js="""() => {
|
||||
const hiddenInput = document.getElementById('canvas-boxes-data');
|
||||
if (hiddenInput) {
|
||||
return hiddenInput.value;
|
||||
}
|
||||
return '';
|
||||
}"""
|
||||
outputs=[image_display, boxes_html, image_index_text, info_text]
|
||||
)
|
||||
|
||||
# Delete box handler (called from HTML button clicks via JS)
|
||||
delete_box_index.change(
|
||||
lambda idx: app.delete_box_by_index(int(idx)) if idx >= 0 else (None, None, None, None),
|
||||
inputs=[delete_box_index],
|
||||
outputs=[image_display, boxes_html, image_index_text, info_text]
|
||||
)
|
||||
|
||||
add_box_btn.click(
|
||||
app.add_box_manual,
|
||||
inputs=[x1_input, y1_input, x2_input, y2_input],
|
||||
outputs=[image_display, boxes_text, info_text]
|
||||
outputs=[image_display, boxes_html, image_index_text, info_text]
|
||||
)
|
||||
|
||||
delete_btn.click(
|
||||
app.delete_last_box,
|
||||
outputs=[image_display, boxes_text, info_text]
|
||||
outputs=[image_display, boxes_html, image_index_text, info_text]
|
||||
)
|
||||
|
||||
clear_btn.click(
|
||||
app.clear_boxes,
|
||||
outputs=[image_display, boxes_text, info_text]
|
||||
outputs=[image_display, boxes_html, image_index_text, info_text]
|
||||
)
|
||||
|
||||
export_btn.click(
|
||||
@ -1614,7 +1682,7 @@ def create_ui(app: AnnotationApp) -> gr.Blocks:
|
||||
)
|
||||
|
||||
# Load first image on start
|
||||
demo.load(on_load, outputs=[image_display, boxes_text, info_text])
|
||||
demo.load(on_load, outputs=[image_display, boxes_html, image_index_text, info_text])
|
||||
|
||||
return demo
|
||||
|
||||
@ -1674,10 +1742,24 @@ def main():
|
||||
print(f"💡 You can change images directory and model weights from the Settings panel")
|
||||
print(f"{'='*60}\n")
|
||||
|
||||
# Combine canvas JS with delete button handler
|
||||
combined_js = CANVAS_GLOBAL_JS + r"""
|
||||
// Wire delete buttons to hidden number input
|
||||
window.deleteBox = function(index) {
|
||||
const hiddenInput = document.querySelector('input[type=number][style*=display], input[type=number].\\!hidden');
|
||||
if (hiddenInput) {
|
||||
hiddenInput.value = index;
|
||||
hiddenInput.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
// Reset after triggering
|
||||
setTimeout(() => { hiddenInput.value = -1; }, 100);
|
||||
}
|
||||
};
|
||||
"""
|
||||
|
||||
demo.launch(
|
||||
server_name="0.0.0.0",
|
||||
server_port=args.port,
|
||||
js=CANVAS_GLOBAL_JS,
|
||||
js=combined_js,
|
||||
share=False
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user