feat: skip track forward on result overlap, not class presence (REQ-189)
Frame holding the tracked class is now annotated like any other; the run's result is dropped only when it intersects a same-class shape already there.
This commit is contained in:
1 parent
3eeb529e3d
commit
f5be7880b0
8 files changed
+86
-30
No files matched your search
@@ -38,6 +38,8 @@ class AssistRequest(BaseModel):
|
||||
box: List[float]
|
||||
class_id: int = 0
|
||||
threshold: float = 0.5
|
||||
# Track Forward only: drop the result when it lands on an existing shape.
|
||||
dedupe: bool = False
|
||||
|
||||
|
||||
class PoolExemplar(BaseModel):
|
||||
@@ -116,7 +118,7 @@ def bulk_reclass(request: BulkReclassRequest) -> dict:
|
||||
def assist(frame_id: int, request: AssistRequest) -> dict:
|
||||
try:
|
||||
return review_store.assist(frame_id, request.box, request.class_id,
|
||||
request.threshold)
|
||||
request.threshold, dedupe=request.dedupe)
|
||||
except review_store.ReviewError as exc:
|
||||
raise HTTPException(400, str(exc))
|
||||
|
||||
|
||||
+1
-5
@@ -137,15 +137,11 @@ def frames(batch_id: int) -> List[dict]:
|
||||
with db.cursor() as cur:
|
||||
cur.execute(
|
||||
"""SELECT f.*, (SELECT COUNT(*) FROM annotations a WHERE a.frame_id = f.id)
|
||||
AS annotation_count,
|
||||
(SELECT GROUP_CONCAT(DISTINCT a.class_id) FROM annotations a
|
||||
WHERE a.frame_id = f.id) AS class_ids
|
||||
AS annotation_count
|
||||
FROM frames f WHERE f.batch_id = ? ORDER BY f.idx""",
|
||||
(batch_id,),
|
||||
)
|
||||
rows = [dict(row) for row in cur.fetchall()]
|
||||
for row in rows:
|
||||
row["class_ids"] = [int(v) for v in row["class_ids"].split(",")] if row["class_ids"] else []
|
||||
return rows
|
||||
|
||||
|
||||
|
||||
+21
-1
@@ -320,7 +320,7 @@ def frames_with_auto(batch_id: int) -> set:
|
||||
|
||||
|
||||
def assist(frame_id: int, box: List[float], class_id: int = 0,
|
||||
threshold: float = 0.5) -> dict:
|
||||
threshold: float = 0.5, dedupe: bool = False) -> dict:
|
||||
"""Drag a rough box, get SAM3's shape for the object inside it (REQ-043).
|
||||
|
||||
The box is a visual exemplar rather than a crop: SAM3 may return several
|
||||
@@ -378,6 +378,16 @@ def assist(frame_id: int, box: List[float], class_id: int = 0,
|
||||
finally:
|
||||
jobs.gpu_lock.release()
|
||||
|
||||
# Track Forward passes dedupe=True (REQ-189): the run still happens, but a
|
||||
# result that lands on a shape already there is the same object twice, so
|
||||
# nothing is written and the caller reports the frame as skipped. Runs after
|
||||
# the lock is released because this part needs no GPU.
|
||||
if dedupe:
|
||||
landed = to_box(geometry)
|
||||
for existing in listing(frame_id):
|
||||
if existing["class_id"] == class_id and intersects(landed, to_box(existing["geometry"])):
|
||||
return {"skipped": True}
|
||||
|
||||
return add(frame_id, class_id, geometry, source="manual", score=detection.score)
|
||||
|
||||
|
||||
@@ -397,6 +407,16 @@ def _overlap(detection_box: List[float], drawn: List[float],
|
||||
return inter / (area_box + area_drawn - inter)
|
||||
|
||||
|
||||
def intersects(a: List[float], b: List[float]) -> bool:
|
||||
"""Any pixel of intersection between two normalized xyxy boxes (REQ-189).
|
||||
|
||||
Deliberately not IoU: two trucks that merely touch are two trucks. Raising
|
||||
this to a ratio is the upgrade path if adjacent same-class boxes ever start
|
||||
getting dropped as duplicates.
|
||||
"""
|
||||
return min(a[2], b[2]) > max(a[0], b[0]) and min(a[3], b[3]) > max(a[1], b[1])
|
||||
|
||||
|
||||
def clear_batch_class_annotations(batch_id: int, class_id: int) -> int:
|
||||
"""Delete all annotations matching class_id across all frames in a batch (REQ-046)."""
|
||||
with db.cursor() as cur:
|
||||
|
||||
Reference in new issue
Block a user