Skip to content

feat(export): QA overlay renderer for exported alert datasets - #381

Merged
Chouffe merged 2 commits into
mainfrom
worktree-vm-export-check
Aug 13, 2026
Merged

feat(export): QA overlay renderer for exported alert datasets#381
Chouffe merged 2 commits into
mainfrom
worktree-vm-export-check

Conversation

@Chouffe

@Chouffe Chouffe commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

What

Adds annotation_api/scripts/data_transfer/export/render_overlays.py (+ make render-overlays), a QA tool that draws the exported boxes back onto the exported frames so annotation work can be eyeballed against the annotator UI.

It reads a dataset written by make export-alerts (manifest.jsonl + images/) and writes <dataset>/overlays/:

overlays/
├── index.csv                 # kind, alert id, sequence id, camera, org, types, frames, boxes, date, sheet
├── smoke/                    # one contact sheet per smoke object
├── false_positive/           # one per sampled false-positive object
└── multi_object/             # one combined sheet per multi-object alert

Each sheet lays an object's frames out in time order. A cell stacks the full frame with its boxes drawn, a magnified crop of the box region, and a caption naming the detection id. The crop row is what makes the sheets usable: exported boxes are typically ~0.05% of frame area, invisible once a 1280x720 frame is scaled into a grid cell.

For a multi-object alert every lane draws on the same image in its own colour, and each lane gets its own zoom panel — a crop around the union of two objects at opposite ends of the frame just reproduces the frame. Sibling lanes hold their own copies of a capture (distinct detection ids, usually distinct bucket_keys), so frames are aligned across lanes by exact recorded_at.

Why

Verifying an export numerically (counts, xyxyn bounds, file presence) says nothing about whether a box actually sits on the plume. This makes that check cheap and repeatable, and the sheet filenames carry the UI's own identifiers (alert{platform_alert_id}_seq{sequence_id}_{camera}.png) so a reviewer can jump straight to the matching lane.

Usage

make render-overlays DATASET_DIR=outputs/alerts_export [FP_SAMPLE=40]

# or directly, for finer control
uv run python -m scripts.data_transfer.export.render_overlays \
  --dataset-dir outputs/alerts_export --mode objects|multi|both \
  --fp-sample 40 --alerts 52124 57057

Every smoke lane is rendered, plus FP_SAMPLE false-positive lanes chosen round-robin across false-positive type combinations so the sample spans all of them before deepening any one. FP_SAMPLE caps only the per-object sheets — the multi-object pass covers every multi-lane alert; --alerts is what bounds a run.

Robustness

The renderer never assumes an image is there or readable. A capture is drawn from whichever lane's copy actually decodes, so on a multi-object alert a sibling rescues a capture the first lane lost. Two failure modes it tolerates, both reachable from a real export:

  • image_path: null — what export_alerts.py writes for a download it could not complete.
  • a present but truncated/0-byte .jpg — never re-downloaded, since the exporter's idempotency check is mere file existence.

Unrenderable captures are skipped with a warning, an alert with no usable image yields no sheet, and index.csv lists only sheets that were actually written.

Verification

  • Ran against a real 767-alert / 17,502-image export from the test server: 92 sheets (35 smoke, 40 false-positive, 17 multi-object), boxes land on the annotated objects in every sheet inspected.
  • Broken-dataset test covering null paths, a deleted file, a 0-byte file, and an alert with no usable image at all: exit 0, no crash, and index.csv matches the PNGs on disk exactly.
  • uv run ruff check . (what the Ruff CI job runs), ruff format --check, and mypy all clean.

No application code is touched — this is scripts, Makefile and docs only.

Renders contact sheets from an alerts_export dataset so annotations can be
eyeballed against the UI: one sheet per object, plus a combined sheet per
multi-object alert where every lane draws on the same image in its own colour.

Each cell pairs the full frame with a magnified crop of the box region --
exported boxes are typically ~0.05% of the frame area and vanish once a
1280x720 frame is scaled to a grid cell. Sibling lanes hold their own copies
of one capture, so multi-object sheets align frames by recorded_at and give
each lane its own zoom panel rather than a union crop.
Review follow-ups on the overlay renderer:

- A capture is now rendered from whichever lane's copy actually decodes.
  Previously a null image_path (which export_alerts writes for a download it
  could not complete) raised TypeError, and a truncated file -- which the
  exporter never re-downloads, its idempotency check being mere existence --
  raised UnidentifiedImageError. Either killed the whole run, losing
  index.csv for every sheet already rendered.
- render_sheet reports whether it wrote anything, so index.csv no longer
  lists sheets that were skipped for want of a usable image.
- Cells are built in a single decode pass; the grid is sized from the
  captures that survived rather than from the manifest.
- Warn when an alert has more lanes than distinct colours.
- Document the tool: make render-overlays target, root CLAUDE.md section,
  and README now points at the shipped export + overlay commands instead of
  calling the pull script "planned".
@Chouffe
Chouffe merged commit 56eafff into main Aug 13, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant