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:
asus committed 2026-10-06 16:08:54 +07:00
1 parent 3eeb529e3d
commit f5be7880b0
8 files changed
+86 -30

No files matched your search

+6 -3
View File
@@ -200,9 +200,12 @@ changes.
shape supplies its own box, a polygon is reduced to its bounding box. Each frame is an
**independent** run: a frame SAM3 refuses (nothing inside the box) is reported and the rest
still run; nothing selected, an unusable shape, or the last frame says so instead of doing
nothing. A frame that **already holds a shape of the tracked class is skipped** — hidden or
dimmed shapes count as present — and the skip is reported in the result banner and marked
with a dot on that frame's filmstrip thumbnail, never silently passed over. A long run can be
nothing. Every frame ahead **runs** — a frame that already holds a shape of the tracked class
is still annotated when SAM3 finds a *different* object there. What is dropped is a
**duplicate**: a result that intersects an existing shape of the tracked class (any shared
area counts, hidden or dimmed shapes count) is not written, and that skip is reported in the
result banner and marked with a dot on that frame's filmstrip thumbnail, never silently passed
over. A long run can be
**cancelled** mid-flight: cancelling stops the remaining frames, but a request already in
flight is not aborted server-side and may still save its shape. The box is never grown and never reseeded from a result, so there is
no size or position readjustment between frames: a fast-moving object can still leave the
+32 -1
View File
@@ -1511,7 +1511,8 @@ read it.
slice bound is computed from `Number(trackFrames)` so a string can never concatenate into the
slice end; `slice(index+1, index+1+N)` and `slice(index+1)` both bounded; cancel lands as
`cancelled — frame n may still be saved by the server` and keeps the shapes already written;
skip check reads the `class_ids` array, not `annotation_count`; marks cleared at the start of
skip check reads the `class_ids` array, not `annotation_count` — *that rule was later replaced
by the post-run overlap test in the next task*; marks cleared at the start of
every run; `Shift+T` deliberately **not** bound (a fat-fingered 500-frame GPU run is not worth
a keystroke). Reviewer pass done — 4 findings fixed. **Still owed, user browser + GPU**: set
N=3 → 3 shapes, reload → N remembered; to-end over the already-reviewed part of batch 19 →
@@ -1534,6 +1535,36 @@ read it.
snapshot left as history); **SAM3 actually finding the object in the next 5 frames needs
the user's GPU run** — select a shape, press `T`, expect a shape on each of the next 5.
## Task — Track Forward skips on overlap, not on class presence (REQ-189 amended) `[DONE]`
- The old rule skipped a frame *before* SAM3 ran when it already held a shape of the tracked
class, which contradicted REQ-189's own "each frame is an independent run": a second truck in
the same frame could never be annotated. Amended to a duplicate test **after** the run.
- `backend/review.py:intersects()` — any intersection between two normalized boxes (shared area
> 0; edge touch is *not* an overlap), deliberately not IoU. `assist()` gained
`dedupe: bool = False`: once the GPU lock is released and right before `add()`, an existing
shape of the same class on that frame whose box intersects the result makes it answer
`{"skipped": True}` and write nothing — no create-then-delete, no transient row.
`backend/api/review.py:AssistRequest.dedupe` carries the flag; `False` keeps the single-shot
box-assist path byte-identical.
- `frontend/src/pages/ReviewPage.jsx:trackForward` — pre-skip on `class_ids` and the local
`class_ids` union are gone, the call sends `dedupe: true`, and `{skipped: true}` lands in the
banner as `skipped K overlapping <class>` plus the filmstrip dot `overlaps an existing <class>`.
- `backend/batches.py:frames()` no longer ships `class_ids` (it was added for the pre-skip and
died with it); `annotation_count` stays.
- Docs: `requirements.md` REQ-189 skip sentence rewritten, `ui-spec.md` banner + thumb tooltip.
- Cost accepted: a long run now pays a GPU call on **every** frame ahead, including frames that
end up skipped, where the old pre-skip made those free. Only worth layering a cheap seed-box
pre-skip back if that hurts.
→ verify: **[DONE]** `uv run python` harness (mocked engine + temp PNG) — 6 `intersects`
cases (edge-touch false, partial/contained/corner true, disjoint false) and 5 `assist()` paths:
dedupe+overlap → `{"skipped": True}` with `add()` never called; other-class box → annotated;
empty frame → annotated; `dedupe=False` → annotated; polygon annotation → skipped.
`batches.frames()` run against the live DB (3737 rows, `class_ids` gone, `annotation_count`
kept); `npm run build` green; `git diff --check` clean. **Still worth a GPU run:** track into a
frame already holding an overlapping truck → banner `skipped K overlapping truck`, dot, count
unchanged, while a frame holding a truck elsewhere gets its own second box.
## Known open points
- *Not closed by any task, by choice:* **any rebuild kills the running job.** Task 14's resume
+7 -4
View File
@@ -937,7 +937,7 @@ worked". The jump happens once per load, and again after an auto-label job finis
### 5.2 Data model in play
```
frame { id, idx, filename, width, height, review_status, annotation_count, class_ids }
frame { id, idx, filename, width, height, review_status, annotation_count }
annotation { id, class_id, geometry, score, source: "auto"|"manual" }
batch { …, review: { pending, approved, rejected }, annotation_count, status }
```
@@ -1036,9 +1036,12 @@ an "asking SAM3…" indicator ·
selected, an unusable shape, or the last frame
says so in the error slot instead of silently doing nothing; a frame SAM3 refuses (nothing
inside the box) does not stop the rest — the banner then **starts** with `Tracked N of M`
(M = frames actually ahead) followed by `skipped K already had <class>` and the per-frame
failures. Skipped frames are those that already hold a shape of the tracked class (hidden or
dimmed counts); they get a dot on their filmstrip thumb and the thumb's tooltip says why. The
(M = frames actually ahead) followed by `skipped K overlapping <class>` and the per-frame
failures. Every frame ahead runs SAM3; a frame is skipped only when its **result** lands on a
shape of the tracked class already there (any intersection — hidden or dimmed counts), which
means a frame holding the tracked class elsewhere still gets its own second box. Skipped frames
get a dot on their filmstrip thumb and the thumb's tooltip says `overlaps an existing <class>`.
The
"asking SAM3…" indicator becomes a **Cancel** button while a run is in flight (Cancel stops the
remaining frames; the frame already in flight can still be saved by the server, and the banner
says so). A second