feat: per-class exemplar preview, class hide toggle, container NMS flag (REQ-182, REQ-183, REQ-184)
This commit is contained in:
1 parent
1a99f2ffe4
commit
96a00267d9
18 files changed
+537
-124
No files matched your search
+49
-8
@@ -10,7 +10,7 @@ calls — see the domain invariants in `../AGENTS.md`.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict, List, Optional
|
||||
from typing import Dict, List, Optional, Set
|
||||
|
||||
from PIL import Image
|
||||
|
||||
@@ -41,11 +41,29 @@ 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,
|
||||
iou_by_class: Optional[Dict[int, float]] = None) -> List[Detection]:
|
||||
"""Greedy NMS per class: highest score wins within the SAME class.
|
||||
def _containment_fraction(box_a: List[float], box_b: List[float]) -> float:
|
||||
"""Intersection over the smaller box's area: how much of the smaller box
|
||||
sits inside the other one (REQ-184's containment test)."""
|
||||
ax0, ay0, ax1, ay1 = box_a
|
||||
bx0, by0, bx1, by1 = box_b
|
||||
inter_w = max(0.0, min(ax1, bx1) - max(ax0, bx0))
|
||||
inter_h = max(0.0, min(ay1, by1) - max(ay0, by0))
|
||||
area_a = max(0.0, ax1 - ax0) * max(0.0, ay1 - ay0)
|
||||
area_b = max(0.0, bx1 - bx0) * max(0.0, by1 - by0)
|
||||
small = min(area_a, area_b)
|
||||
return inter_w * inter_h / small if small > 0 else 0.0
|
||||
|
||||
`iou_by_class` overrides the threshold per class id (REQ-181)."""
|
||||
|
||||
def deduplicate(detections: List[Detection], iou_threshold: float = 0.8,
|
||||
iou_by_class: Optional[Dict[int, float]] = None,
|
||||
container_ids: Optional[Set[int]] = None) -> List[Detection]:
|
||||
"""Greedy NMS: highest score wins within the SAME class, then across
|
||||
classes (REQ-031) — unless the kept box is a container class and the
|
||||
candidate is at least 90% inside it, which is containment, not overlap
|
||||
(REQ-184).
|
||||
|
||||
`iou_by_class` overrides the threshold per class id (REQ-181);
|
||||
`container_ids` are the class ids marked container."""
|
||||
if not iou_by_class and iou_threshold <= 0.0:
|
||||
return detections
|
||||
|
||||
@@ -65,7 +83,26 @@ def deduplicate(detections: List[Detection], iou_threshold: float = 0.8,
|
||||
if all(_iou(det.box, k.box) < iou for k in cls_kept):
|
||||
cls_kept.append(det)
|
||||
kept.extend(cls_kept)
|
||||
return kept
|
||||
|
||||
# Cross-class pass (REQ-031), greedy score-desc. A pair whose resolved
|
||||
# threshold is <= 0 is never suppressed — zero means NMS off for that
|
||||
# class, mirroring the within-class pass above.
|
||||
cross_kept: List[Detection] = []
|
||||
for det in sorted(kept, key=lambda d: d.score, reverse=True):
|
||||
drop = False
|
||||
for k in cross_kept:
|
||||
if det.class_id == k.class_id:
|
||||
continue
|
||||
if (container_ids and k.class_id in container_ids
|
||||
and _containment_fraction(det.box, k.box) >= 0.9):
|
||||
continue
|
||||
iou = (iou_by_class or {}).get(det.class_id, iou_threshold)
|
||||
if iou > 0.0 and _iou(det.box, k.box) >= iou:
|
||||
drop = True
|
||||
break
|
||||
if not drop:
|
||||
cross_kept.append(det)
|
||||
return cross_kept
|
||||
|
||||
|
||||
def label_image(
|
||||
@@ -80,13 +117,16 @@ def label_image(
|
||||
thresholds: Optional[List[float]] = None,
|
||||
iou_by_class: Optional[Dict[int, float]] = None,
|
||||
min_box_fracs: Optional[List[float]] = None,
|
||||
container_ids: Optional[Set[int]] = 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.
|
||||
`thresholds`, `iou_by_class` and `min_box_fracs` are per-prompt overrides
|
||||
aligned with `prompts` (REQ-181); classes without one use the global values."""
|
||||
aligned with `prompts` (REQ-181); classes without one use the global values.
|
||||
`container_ids` are prompt indices marked container (REQ-184) — the caller
|
||||
maps them, because detections still carry prompt-index class ids here."""
|
||||
try:
|
||||
image = Image.open(image_path).convert("RGB")
|
||||
except Exception as exc: # unreadable/corrupt frame: report, don't abort the job
|
||||
@@ -121,4 +161,5 @@ def label_image(
|
||||
]
|
||||
|
||||
return ImageResult(image_path, rel_path, width, height,
|
||||
deduplicate(detections, iou_threshold, iou_by_class=iou_by_class))
|
||||
deduplicate(detections, iou_threshold, iou_by_class=iou_by_class,
|
||||
container_ids=container_ids))
|
||||
Reference in new issue
Block a user