feat: per-class autolabel params + frame-scoped class clear (REQ-180, REQ-181)
- REQ-181: class_params {name: {threshold?, iou_threshold?, min_box_frac?}}
on /preview and /autolabel, per-class override table in both modals;
empty overrides take the unchanged global path
- REQ-180: x button on each review sidebar class row clears that class on
the current frame only via bulk-delete, no confirmation
- includes REQ-178 empty date-folder cycle fix (archive_index.py)
This commit is contained in:
1 parent
c244ab9795
commit
1a99f2ffe4
16 files changed
+372
-45
No files matched your search
+33
-11
@@ -10,7 +10,7 @@ calls — see the domain invariants in `../AGENTS.md`.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Optional
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from PIL import Image
|
||||
|
||||
@@ -41,9 +41,12 @@ def _iou(box_a: List[float], box_b: List[float]) -> float:
|
||||
return inter / union if union > 0 else 0.0
|
||||
|
||||
|
||||
def deduplicate(detections: List[Detection], iou_threshold: float = 0.8) -> List[Detection]:
|
||||
"""Greedy NMS per class: highest score wins within the SAME class."""
|
||||
if iou_threshold <= 0.0:
|
||||
def deduplicate(detections: List[Detection], iou_threshold: float = 0.8,
|
||||
iou_by_class: Optional[Dict[int, float]] = None) -> List[Detection]:
|
||||
"""Greedy NMS per class: highest score wins within the SAME class.
|
||||
|
||||
`iou_by_class` overrides the threshold per class id (REQ-181)."""
|
||||
if not iou_by_class and iou_threshold <= 0.0:
|
||||
return detections
|
||||
|
||||
by_class: dict[int, List[Detection]] = {}
|
||||
@@ -52,10 +55,14 @@ def deduplicate(detections: List[Detection], iou_threshold: float = 0.8) -> List
|
||||
by_class.setdefault(det.class_id, []).append(det)
|
||||
|
||||
kept: List[Detection] = []
|
||||
for cls_dets in by_class.values():
|
||||
for cls, cls_dets in by_class.items():
|
||||
iou = (iou_by_class or {}).get(cls, iou_threshold)
|
||||
if iou <= 0.0:
|
||||
kept.extend(cls_dets)
|
||||
continue
|
||||
cls_kept: List[Detection] = []
|
||||
for det in sorted(cls_dets, key=lambda d: d.score, reverse=True):
|
||||
if all(_iou(det.box, k.box) < iou_threshold for k in cls_kept):
|
||||
if all(_iou(det.box, k.box) < iou for k in cls_kept):
|
||||
cls_kept.append(det)
|
||||
kept.extend(cls_kept)
|
||||
return kept
|
||||
@@ -70,11 +77,16 @@ def label_image(
|
||||
min_box_frac: float = 0.0,
|
||||
exemplar_index: int = -1,
|
||||
exemplars: Optional[List[dict]] = None,
|
||||
thresholds: Optional[List[float]] = None,
|
||||
iou_by_class: Optional[Dict[int, float]] = None,
|
||||
min_box_fracs: Optional[List[float]] = None,
|
||||
) -> ImageResult:
|
||||
"""Detect every prompt in one image and return the surviving instances.
|
||||
|
||||
When `exemplars` are given, the prompt at `exemplar_index` also carries them
|
||||
as drawn box exemplars (REQ-172); every other prompt runs on text alone."""
|
||||
as drawn box exemplars (REQ-172); every other prompt runs on text alone.
|
||||
`thresholds`, `iou_by_class` and `min_box_fracs` are per-prompt overrides
|
||||
aligned with `prompts` (REQ-181); classes without one use the global values."""
|
||||
try:
|
||||
image = Image.open(image_path).convert("RGB")
|
||||
except Exception as exc: # unreadable/corrupt frame: report, don't abort the job
|
||||
@@ -84,14 +96,24 @@ def label_image(
|
||||
try:
|
||||
if exemplars and 0 <= exemplar_index < len(prompts):
|
||||
detections = get_engine().detect_with_exemplars(
|
||||
image, prompts, threshold, exemplar_index, exemplars
|
||||
image, prompts, threshold, exemplar_index, exemplars,
|
||||
thresholds=thresholds
|
||||
)
|
||||
else:
|
||||
detections = get_engine().detect(image, prompts, threshold)
|
||||
detections = get_engine().detect(image, prompts, threshold,
|
||||
thresholds=thresholds)
|
||||
except Exception as exc:
|
||||
return ImageResult(image_path, rel_path, width, height, error=str(exc))
|
||||
|
||||
if min_box_frac > 0:
|
||||
if min_box_fracs is not None:
|
||||
def _keep(det) -> bool:
|
||||
frac = min_box_fracs[det.class_id] if det.class_id < len(min_box_fracs) else min_box_frac
|
||||
if frac <= 0:
|
||||
return True
|
||||
floor = width * height * frac
|
||||
return (det.box[2] - det.box[0]) * (det.box[3] - det.box[1]) >= floor
|
||||
detections = [d for d in detections if _keep(d)]
|
||||
elif min_box_frac > 0:
|
||||
floor = width * height * min_box_frac
|
||||
detections = [
|
||||
d for d in detections
|
||||
@@ -99,4 +121,4 @@ def label_image(
|
||||
]
|
||||
|
||||
return ImageResult(image_path, rel_path, width, height,
|
||||
deduplicate(detections, iou_threshold))
|
||||
deduplicate(detections, iou_threshold, iou_by_class=iou_by_class))
|
||||
Reference in new issue
Block a user