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:
1 parent
dee58e4ae5
commit
d3a6aa49b6
13 files changed
+90
-25
No files matched your search
@@ -56,6 +56,7 @@ class ExemplarLabelRequest(BaseModel):
|
||||
threshold: float = 0.5
|
||||
iou_threshold: float = 0.8
|
||||
min_box_frac: float = 0.002
|
||||
max_box_frac: float = 1.0
|
||||
max_detections: int = 100
|
||||
# Off by default: a drag previews, only Apply writes.
|
||||
apply: bool = False
|
||||
@@ -131,6 +132,7 @@ def exemplar_label(frame_id: int, request: ExemplarLabelRequest) -> dict:
|
||||
threshold=request.threshold,
|
||||
iou_threshold=request.iou_threshold,
|
||||
min_box_frac=request.min_box_frac,
|
||||
max_box_frac=request.max_box_frac,
|
||||
max_detections=request.max_detections,
|
||||
apply=request.apply,
|
||||
)
|
||||
|
||||
@@ -27,7 +27,7 @@ def _parse_class_params(raw) -> dict:
|
||||
if not isinstance(values, dict):
|
||||
continue
|
||||
entry = {}
|
||||
for key in ("threshold", "iou_threshold", "min_box_frac"):
|
||||
for key in ("threshold", "iou_threshold", "min_box_frac", "max_box_frac"):
|
||||
if key in values:
|
||||
try:
|
||||
entry[key] = float(values[key])
|
||||
@@ -229,6 +229,9 @@ def _run_autolabel(job) -> None:
|
||||
mb = cp.get("min_box_frac")
|
||||
if mb and mb > 0 and (xyxyn[2]-xyxyn[0])*(xyxyn[3]-xyxyn[1]) < mb:
|
||||
continue
|
||||
xb = cp.get("max_box_frac")
|
||||
if xb and xb < 1 and (xyxyn[2]-xyxyn[0])*(xyxyn[3]-xyxyn[1]) > xb:
|
||||
continue
|
||||
|
||||
all_raw_detections.append(labeling.Detection(
|
||||
class_id=target_class_id,
|
||||
@@ -240,7 +243,7 @@ def _run_autolabel(job) -> None:
|
||||
|
||||
if selected_engine == "sam3" and sam3_target_classes:
|
||||
prompts = [(c.get("prompt") or c["name"]).strip() for c in sam3_target_classes]
|
||||
thr_list = iou_list = mb_list = None
|
||||
thr_list = iou_list = mb_list = mx_list = None
|
||||
if per_class:
|
||||
names = [c["name"].strip().lower() for c in sam3_target_classes]
|
||||
if any("threshold" in v for v in per_class.values()):
|
||||
@@ -249,12 +252,15 @@ def _run_autolabel(job) -> None:
|
||||
iou_list = [per_class.get(n, {}).get("iou_threshold", iou_thresh) for n in names]
|
||||
if any("min_box_frac" in v for v in per_class.values()):
|
||||
mb_list = [per_class.get(n, {}).get("min_box_frac", job.params.get("min_box_frac", 0.0)) for n in names]
|
||||
if any("max_box_frac" in v for v in per_class.values()):
|
||||
mx_list = [per_class.get(n, {}).get("max_box_frac", 1.0) for n in names]
|
||||
res = labeling.label_image(
|
||||
frame_file, frame["filename"], prompts, conf,
|
||||
iou_threshold=iou_thresh, min_box_frac=job.params.get("min_box_frac", 0.0),
|
||||
thresholds=thr_list,
|
||||
iou_by_class=dict(enumerate(iou_list)) if iou_list else None,
|
||||
min_box_fracs=mb_list,
|
||||
max_box_fracs=mx_list,
|
||||
container_ids=prompt_container_ids,
|
||||
)
|
||||
if not res.error and res.detections:
|
||||
|
||||
+9
-3
@@ -37,6 +37,7 @@ DEFAULTS = {
|
||||
"threshold": 0.5,
|
||||
"iou_threshold": 0.8,
|
||||
"min_box_frac": 0.002,
|
||||
"max_box_frac": 1.0,
|
||||
"max_detections": 100,
|
||||
}
|
||||
|
||||
@@ -116,7 +117,8 @@ def _drop_negative_overlaps(frame_id: int, class_id: int,
|
||||
|
||||
def label(frame_id: int, class_id: int, exemplars: List[dict],
|
||||
threshold: float = 0.5, iou_threshold: float = 0.8,
|
||||
min_box_frac: float = 0.002, max_detections: int = 100,
|
||||
min_box_frac: float = 0.002, max_box_frac: float = 1.0,
|
||||
max_detections: int = 100,
|
||||
apply: bool = False) -> dict:
|
||||
"""Detect one class on one frame from the frame's exemplar pool.
|
||||
|
||||
@@ -189,12 +191,16 @@ def label(frame_id: int, class_id: int, exemplars: List[dict],
|
||||
finally:
|
||||
jobs.gpu_lock.release()
|
||||
|
||||
# The panel's filters, in the order the batch job applies them (REQ-175):
|
||||
# area floor, then NMS, then the cap on how many survive.
|
||||
# The panel's filters, in the order the batch job applies them (REQ-175/188):
|
||||
# area floor, then ceiling, then NMS, then the cap on how many survive.
|
||||
if min_box_frac > 0:
|
||||
floor = width * height * min_box_frac
|
||||
found = [d for d in found
|
||||
if (d.box[2] - d.box[0]) * (d.box[3] - d.box[1]) >= floor]
|
||||
if 0 < max_box_frac < 1:
|
||||
ceiling = width * height * max_box_frac
|
||||
found = [d for d in found
|
||||
if (d.box[2] - d.box[0]) * (d.box[3] - d.box[1]) <= ceiling]
|
||||
found = deduplicate(found, iou_threshold)
|
||||
found.sort(key=lambda d: d.score, reverse=True)
|
||||
if max_detections > 0:
|
||||
|
||||
+21
-2
@@ -112,19 +112,23 @@ def label_image(
|
||||
threshold: float,
|
||||
iou_threshold: float = 0.8,
|
||||
min_box_frac: float = 0.0,
|
||||
max_box_frac: float = 1.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,
|
||||
max_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.
|
||||
`thresholds`, `iou_by_class`, `min_box_fracs` and `max_box_fracs` are
|
||||
per-prompt overrides aligned with `prompts` (REQ-181); classes without one
|
||||
use the global values. `max_box_frac(s)` is the per-class size ceiling,
|
||||
1.0 = off (REQ-188).
|
||||
`container_ids` are prompt indices marked container (REQ-184) — the caller
|
||||
maps them, because detections still carry prompt-index class ids here."""
|
||||
try:
|
||||
@@ -160,6 +164,21 @@ def label_image(
|
||||
if (d.box[2] - d.box[0]) * (d.box[3] - d.box[1]) >= floor
|
||||
]
|
||||
|
||||
if max_box_fracs is not None:
|
||||
def _keep_max(det) -> bool:
|
||||
frac = max_box_fracs[det.class_id] if det.class_id < len(max_box_fracs) else max_box_frac
|
||||
if frac >= 1 or frac <= 0:
|
||||
return True
|
||||
ceiling = width * height * frac
|
||||
return (det.box[2] - det.box[0]) * (det.box[3] - det.box[1]) <= ceiling
|
||||
detections = [d for d in detections if _keep_max(d)]
|
||||
elif 0 < max_box_frac < 1:
|
||||
ceiling = width * height * max_box_frac
|
||||
detections = [
|
||||
d for d in detections
|
||||
if (d.box[2] - d.box[0]) * (d.box[3] - d.box[1]) <= ceiling
|
||||
]
|
||||
|
||||
return ImageResult(image_path, rel_path, width, height,
|
||||
deduplicate(detections, iou_threshold, iou_by_class=iou_by_class,
|
||||
container_ids=container_ids))
|
||||
+7
-1
@@ -120,6 +120,9 @@ def preview_frame(
|
||||
mb = cp.get("min_box_frac")
|
||||
if mb and mb > 0 and (xyxyn[2]-xyxyn[0])*(xyxyn[3]-xyxyn[1]) < mb:
|
||||
continue
|
||||
xb = cp.get("max_box_frac")
|
||||
if xb and xb < 1 and (xyxyn[2]-xyxyn[0])*(xyxyn[3]-xyxyn[1]) > xb:
|
||||
continue
|
||||
|
||||
all_raw_detections.append(labeling.Detection(
|
||||
class_id=target_class_id,
|
||||
@@ -143,7 +146,7 @@ def preview_frame(
|
||||
if c["name"].strip().lower() == wanted),
|
||||
-1,
|
||||
)
|
||||
thr_list = iou_list = mb_list = None
|
||||
thr_list = iou_list = mb_list = mx_list = None
|
||||
if per_class:
|
||||
names = [c["name"].strip().lower() for c in sam3_target_classes]
|
||||
if any("threshold" in v for v in per_class.values()):
|
||||
@@ -152,6 +155,8 @@ def preview_frame(
|
||||
iou_list = [per_class.get(n, {}).get("iou_threshold", iou_threshold) for n in names]
|
||||
if any("min_box_frac" in v for v in per_class.values()):
|
||||
mb_list = [per_class.get(n, {}).get("min_box_frac", min_box_frac) for n in names]
|
||||
if any("max_box_frac" in v for v in per_class.values()):
|
||||
mx_list = [per_class.get(n, {}).get("max_box_frac", 1.0) for n in names]
|
||||
res = labeling.label_image(
|
||||
frame_file, frame["filename"], prompts, threshold,
|
||||
iou_threshold=iou_threshold, min_box_frac=min_box_frac,
|
||||
@@ -159,6 +164,7 @@ def preview_frame(
|
||||
thresholds=thr_list,
|
||||
iou_by_class=dict(enumerate(iou_list)) if iou_list else None,
|
||||
min_box_fracs=mb_list,
|
||||
max_box_fracs=mx_list,
|
||||
container_ids=prompt_container_ids,
|
||||
)
|
||||
if not res.error and res.detections:
|
||||
|
||||
+7
-7
@@ -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
|
||||
|
||||
@@ -1371,6 +1371,24 @@ read it.
|
||||
raw path in `list_models` payload, `secondary_model_path` outside REQ-187 scope,
|
||||
non-str TypeError unreachable); no DB rewrite needed — resolver covers legacy rows.
|
||||
|
||||
## Task — per-class max box fraction (REQ-188) `[DONE]`
|
||||
|
||||
1. `max_box_frac` (default `1.0` = off) mirrored 1:1 over every `min_box_frac` site:
|
||||
`labeling.label_image` gains `max_box_frac`/`max_box_fracs` with a ceiling block right
|
||||
after the floor (before dedup), guard `frac >= 1 or frac <= 0` → keep; `preview.py` and
|
||||
`autolabel.py` gain the inline `xyxyn` gate (`xb and xb < 1`) and the `mx_list` per-class
|
||||
build (fallback `1.0`) passed as `max_box_fracs=`; `_parse_class_params` accepts the key;
|
||||
`exemplar.label` filters inline (it does not delegate to `label_image`) with
|
||||
`0 < max_box_frac < 1` between floor and NMS; `ExemplarLabelRequest` carries the explicit
|
||||
review-filter field through to `exemplar_store.label`. Frontend: `MaxBox` added to
|
||||
`ClassParamsTable` KEYS (so `buildClassParams` sends it automatically), `maxbox <v>`
|
||||
inserted in the copy line per amended REQ-186, `max_box_frac: 1.0` in both modals'
|
||||
globals, `Max box size` slider in `ExemplarFilterPanel` + `FILTER_DEFAULTS` —
|
||||
verify: **[DONE]** import smoke exit 0; `npm run build` green (520 ms); behavioral harness
|
||||
over `label_image` prints the 8 cases (off/0/1/per-class/list-fallback/floor+ceiling)
|
||||
with expected drops; node eval proves `buildClassParams` emits `max_box_frac`;
|
||||
`git diff --stat` = 5 backend + 5 frontend files + docs.
|
||||
|
||||
## Known open points
|
||||
|
||||
- *Not closed by any task, by choice:* **any rebuild kills the running job.** Task 14's resume
|
||||
|
||||
+8
-7
@@ -450,7 +450,7 @@ Two columns inside one dialog (920 px wide, max 96 vw / 90 vh, scrollable).
|
||||
- NMS IoU threshold — 0…0.9 step 0.05, default 0.0.
|
||||
- Min box size (fraction of frame) — 0…0.5 step 0.005, default 0, shown as a percentage.
|
||||
- **Per-class overrides** (REQ-181) — a compact table below the sliders, one row per currently
|
||||
selected class: `Conf` / `IoU` / `MinBox` number inputs and a **Container** checkbox
|
||||
selected class: `Conf` / `IoU` / `MinBox` / `MaxBox` number inputs and a **Container** checkbox
|
||||
(REQ-184). Each input's placeholder shows the current global value and an empty input
|
||||
inherits it; only filled cells are sent, as
|
||||
`class_params` on **both** the preview and the job request, so preview and run cannot
|
||||
@@ -465,7 +465,7 @@ Two columns inside one dialog (920 px wide, max 96 vw / 90 vh, scrollable).
|
||||
- **Copy (REQ-186)** — small right-aligned ghost button above the overrides table; label
|
||||
`Copy`, flips to `Copied` for 1.5 s inside an `aria-live` span, and on failure silently
|
||||
stays `Copy`. One line per selected class, e.g.
|
||||
`class sack conf 0.4 iou 0.6 minbox 0.005 container false` — an empty input copies the
|
||||
`class sack conf 0.4 iou 0.6 minbox 0.005 maxbox 1 container false` — an empty input copies the
|
||||
global value. Present in the engine-chooser modal **and** the mass modal (shared
|
||||
component).
|
||||
- `ClassPromptPanel`:
|
||||
@@ -1024,13 +1024,14 @@ over the frame** — the shapes it governs are drawn on the canvas, and a card o
|
||||
covers the thing being judged. It scrolls itself into view on mount, because the sidebar
|
||||
scrolls and a decision the user cannot see is not a decision.
|
||||
|
||||
Its four sliders, re-previewing on a 250 ms debounce:
|
||||
Its five sliders, re-previewing on a 250 ms debounce:
|
||||
|
||||
| Key | Label | Range | Default | Hint |
|
||||
|---|---|---|---|---|
|
||||
| `threshold` | Confidence | 0.05–0.95 | 0.5 | lower finds more, and more junk |
|
||||
| `iou_threshold` | Overlap (NMS) | 0.1–1 | 0.8 | boxes overlapping this much are one object; lower deletes more |
|
||||
| `min_box_frac` | Min box size | 0–0.05 | 0.002 | shown as % of frame; higher deletes more |
|
||||
| `max_box_frac` | Max box size | 0–1 | 1.0 | shown as % of frame; boxes bigger than this are junk, 100% = off |
|
||||
| `max_detections` | Max shapes | 5–300 | 100 | keep only the highest-scoring N |
|
||||
|
||||
Panel header: `Auto-label "<class>"` + *Undo* (drops the last example drawn). Status line:
|
||||
@@ -1062,7 +1063,7 @@ POST /api/frames/{id}/exemplar-label
|
||||
{ "exemplars": [{ "box": [x0,y0,x1,y1], "positive": true }],
|
||||
"class_id": 0,
|
||||
"threshold": 0.5, "iou_threshold": 0.8,
|
||||
"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,
|
||||
@@ -1072,7 +1073,7 @@ POST /api/frames/{id}/exemplar-label
|
||||
Server-side, positives become `source="manual"` and survive a batch re-run; what SAM3 adds
|
||||
becomes `source="auto"` and is replaceable. Detections overlapping a negative by ≥0.3 are
|
||||
dropped; ones overlapping a positive by ≥0.6 are dropped as already covered. The panel's
|
||||
filters run first, in the batch job's order: area floor → NMS → cap.
|
||||
filters run first, in the batch job's order: area floor → ceiling → NMS → cap.
|
||||
|
||||
**GPU-lock timeout is 5 s here** (versus 20 s for `assist`) because this fires from a mouse
|
||||
gesture. On timeout the response carries `redetected: false`, a message the panel shows, and
|
||||
@@ -1527,8 +1528,8 @@ Shortcuts).
|
||||
- Dua mode: **Draw** / **Select** (`V`). Select mode tidak pernah mengubah geometri; Draw mode
|
||||
tidak marquee.
|
||||
- **Di draw mode, drag bukan menggambar kotak — itu adalah *contoh*.** User menggambar satu
|
||||
instance, SAM3 mencari sisanya, hasilnya di-*preview*, di-*tune* lewat 4 slider (Confidence,
|
||||
Overlap/NMS, Min box size, Max shapes), lalu baru **Apply** yang menulis. Shift-drag = contoh
|
||||
instance, SAM3 mencari sisanya, hasilnya di-*preview*, di-*tune* lewat 5 slider (Confidence,
|
||||
Overlap/NMS, Min box size, Max box size, Max shapes), lalu baru **Apply** yang menulis. Shift-drag = contoh
|
||||
negatif ("bukan ini"). Panel filter duduk di **sidebar**, bukan menutupi frame.
|
||||
- Tahan `S` = SAM3 click-assist (satu box → satu shape).
|
||||
- Keyboard-first: `V ← → A X U N C T 1–9 Del H Esc Enter`. Semua aksi punya shortcut; panel
|
||||
|
||||
@@ -364,7 +364,7 @@ export default function AutoAnnotateModal({
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<span className="hint" style={{ fontSize: '0.8rem' }}>Per-class overrides (empty = global):</span>
|
||||
<div style={{ marginTop: 4 }}>
|
||||
<ClassParamsTable classNames={selectedClasses} globals={{ threshold, iou_threshold: iouThreshold, min_box_frac: minBoxFrac }} value={classParams} onChange={setClassParams} project={project} />
|
||||
<ClassParamsTable classNames={selectedClasses} globals={{ threshold, iou_threshold: iouThreshold, min_box_frac: minBoxFrac, max_box_frac: 1.0 }} value={classParams} onChange={setClassParams} project={project} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ const KEYS = [
|
||||
{ key: 'threshold', label: 'Conf', step: 0.05, color: '#38bdf8' },
|
||||
{ key: 'iou_threshold', label: 'IoU', step: 0.05, color: '#c084fc' },
|
||||
{ key: 'min_box_frac', label: 'MinBox', step: 0.005, color: '#34d399' },
|
||||
{ key: 'max_box_frac', label: 'MaxBox', step: 0.05, color: '#f472b6' },
|
||||
]
|
||||
|
||||
export function buildClassParams(classParams) {
|
||||
@@ -66,7 +67,9 @@ export default function ClassParamsTable({ classNames, globals, value, onChange,
|
||||
(name) =>
|
||||
`class ${name} conf ${fmt(effective(name, 'threshold'))} iou ${fmt(
|
||||
effective(name, 'iou_threshold')
|
||||
)} minbox ${fmt(effective(name, 'min_box_frac'))} container ${containers.has(name)}`
|
||||
)} minbox ${fmt(effective(name, 'min_box_frac'))} maxbox ${fmt(
|
||||
effective(name, 'max_box_frac')
|
||||
)} container ${containers.has(name)}`
|
||||
)
|
||||
.join('\n')
|
||||
const copy = async () => {
|
||||
|
||||
@@ -19,6 +19,9 @@ const SLIDERS = [
|
||||
{ key: 'min_box_frac', label: 'Min box size', min: 0, max: 0.05, step: 0.001,
|
||||
color: '#34d399', format: (v) => `${(v * 100).toFixed(1)}% of frame`,
|
||||
hint: 'Boxes smaller than this are specks. Higher deletes more.' },
|
||||
{ key: 'max_box_frac', label: 'Max box size', min: 0, max: 1, step: 0.05,
|
||||
color: '#f472b6', format: (v) => `${(v * 100).toFixed(0)}% of frame`,
|
||||
hint: 'Boxes bigger than this are junk. 100% = off.' },
|
||||
{ key: 'max_detections', label: 'Max shapes', min: 5, max: 300, step: 5,
|
||||
color: '#fbbf24', format: (v) => String(v),
|
||||
hint: 'Keep only the highest-scoring N.' },
|
||||
|
||||
@@ -250,7 +250,7 @@ export default function MassAutoAnnotateModal({ batches, project, onClose, onSuc
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<span className="hint" style={{ fontSize: '0.8rem' }}>Per-class overrides (empty = global):</span>
|
||||
<div style={{ marginTop: 4 }}>
|
||||
<ClassParamsTable classNames={selectedClasses} globals={{ threshold, iou_threshold: iouThreshold, min_box_frac: minBoxFrac }} value={classParams} onChange={setClassParams} project={project} />
|
||||
<ClassParamsTable classNames={selectedClasses} globals={{ threshold, iou_threshold: iouThreshold, min_box_frac: minBoxFrac, max_box_frac: 1.0 }} value={classParams} onChange={setClassParams} project={project} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ export const FILTER_DEFAULTS = {
|
||||
threshold: 0.5,
|
||||
iou_threshold: 0.8,
|
||||
min_box_frac: 0.002,
|
||||
max_box_frac: 1.0,
|
||||
max_detections: 100,
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user