feat: per-class max box fraction (REQ-188)

- labeling/preview/autolabel: max_box_frac + per-class max_box_fracs
  ceiling filter (0=none, 1=off) before NMS, mirrors min_box_frac
- exemplar review-assist: ceiling before max_detections truncation;
  ExemplarLabelRequest + filter panel 'Max box size' slider
- ClassParamsTable: MaxBox column; copy line now REQ-186 amended
  order: class <name> conf <v> iou <v> minbox <v> maxbox <v> container
- docs: design/ui-spec/tasks updated (incl. stale 4-slider narrative)
This commit is contained in:
asus committed 2026-10-02 17:27:11 +07:00
1 parent dee58e4ae5
commit d3a6aa49b6
13 files changed
+90 -25

No files matched your search

+7 -7
View File
@@ -181,7 +181,7 @@ GET /api/batches/{id} # status + review progress (REQ-
GET /api/batches/{id}/frames # frames + statuses
POST /api/batches/{id}/autolabel # {threshold, class_params} → job (REQ-030,032,034,181)
POST /api/batches/{id}/preview # one frame, run now, nothing written;
# +class_params {name: {threshold?, iou_threshold?, min_box_frac?}}
# +class_params {name: {threshold?, iou_threshold?, min_box_frac?, max_box_frac?}}
# empty override = global value, keys are class names (REQ-181)
# +exemplars[] {box:[cx,cy,w,h], positive} — the touched class's own pool only
# +exemplar_class_name, +target_class_names; per-class, accumulating
@@ -249,10 +249,10 @@ updated. Range and fps live on the batch, so one video can be used repeatedly.
**autolabel (REQ-030…034).** Per frame: one `set_image`, then loop each class's prompt (see
the domain invariants in `../AGENTS.md`), cross-class greedy NMS (REQ-031), write `annotations` rows with
`source='auto'`. `class_params` (REQ-181) is consulted per class: a numeric
`threshold` / `iou_threshold` / `min_box_frac` entry replaces the job's global for that class
`threshold` / `iou_threshold` / `min_box_frac` / `max_box_frac` entry replaces the job's global for that class
only; classes without an entry — and a run with `class_params` empty or absent — take the
exact global path. The YOLO confidence floor uses `min([global] + overrides)`, the SAM3
threshold/dedup/min-box lists are built only from classes that actually override that key,
threshold/dedup/min-box/max-box lists are built only from classes that actually override that key,
and `/preview` applies the same overrides so the preview and the job cannot disagree.
The job and `/preview` also read `project_classes.container` per class when building the
container-id set for the NMS containment carve-out — same stored flag for both (REQ-184,
@@ -363,7 +363,7 @@ per-class overrides table also carries a **Container** checkbox (REQ-184) that t
`project_classes.container` through `PATCH /api/projects/{id} { containers: … }`. The same
table carries a **Copy** button (REQ-186): one click puts every selected class's effective
settings (per-class override where set, else the global slider; container = the checkbox
state) on the clipboard as short lines `class <name> conf … iou … minbox … container
state) on the clipboard as short lines `class <name> conf … iou … minbox … maxbox … container
true|false`, through `clipboard.js`'s `copyText` (Clipboard API with an `execCommand`
fallback for insecure contexts) — read-only, no network, no setting changed. Because
`ClassParamsTable` is shared, the mass modal carries the button too.
@@ -394,7 +394,7 @@ with the shortened pool.
The run is a **dry run by default** (REQ-175). `ExemplarFilterPanel.jsx` is passed to
`ReviewSidebar` and rendered above the class list — not over the frame, which is where its
proposals are drawn. It is mounted only while a run is undecided (`pool.active`): the first
drag opens it, Apply and Discard close it, and the pool outlives it. Its four sliders
drag opens it, Apply and Discard close it, and the pool outlives it. Its five sliders
re-preview on a 250 ms debounce. While a preview is up the canvas **hides the stored shapes
of the class under review** — the run replaces them wholesale, and leaving them on screen made
a rejected detection look like it had never gone; other classes stay, dimmed
@@ -419,7 +419,7 @@ POST /api/frames/{id}/exemplar-label
{ "exemplars": [{"box": [x0,y0,x1,y1], "positive": true}, …], # normalized xyxy
"class_id": 0,
"threshold": 0.5, "iou_threshold": 0.8, # the panel (REQ-175)
"min_box_frac": 0.002, "max_detections": 100,
"min_box_frac": 0.002, "max_box_frac": 1.0, "max_detections": 100,
"apply": false }
→ { "shapes": [{"geometry": …, "score": …, "source": "manual"|"auto"}, …],
"applied": false, "redetected": true, "message": null,
@@ -440,7 +440,7 @@ then rewrites the frame **for that class only**:
while the drawn shapes survive.
The panel's filters run before any of that, in the order the batch job uses them: area floor,
then NMS (`labeling.deduplicate`), then the cap on how many survive.
then ceiling (REQ-188), then NMS (`labeling.deduplicate`), then the cap on how many survive.
The delete-and-reinsert happens in one `db.cursor()` transaction, so the frame is never
briefly empty. Other classes on the frame are never touched. The GPU lock is taken with a