Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 2 additions & 40 deletions .claude/skills/mblt-vision/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,47 +28,9 @@ description: >-
Use file_cfg.filename for MXQ and derive the same-stem ONNX artifact unless
onnx_filename is required.
- Every post_cfg declares dataset; resolve output taxonomy from the dataset/task pair.
- A promptable or multi-artifact model (see mask_generation/SAM2HieraLarge) bypasses
MBLT_Engine.__init__, build_preprocess/build_postprocess, and create_model_class entirely,
implementing its own preprocess/predict methods; it still subclasses MBLT_Engine only for
list_models() discovery. Reuse wrapper.download_hub_artifact for any additional Hub artifact
- Reuse wrapper.download_hub_artifact for any model that needs more than one Hub artifact
rather than duplicating Hub-resolution logic.
- mask_generation supports framework="mxq" (default) and framework="onnx" with
MBLT_Engine-style inference/conflict semantics (explicit encoder/decoder path suffixes infer
the framework; NPU-only arguments are ignored for ONNX). The ONNX exports are same-stem
sam2_hiera_large_{encoder,decoder}.onnx at the Hub repo root (board-agnostic). Keep the
graph contracts pinned in _sam2_contracts.py and validated at construction: two MXQ decoder
generations exist, identified from the artifact's declared input shapes at engine
construction (detect_decoder_contract) -- assembled (six host-flattened inputs, four
outputs) and bridged (prompt-encoder raw inputs, two outputs; sam_tokens/object_score are
optional in classify_decoder_outputs); the ONNX graphs use the direct-MBLT NHWC encoder
and six named decoder inputs with a dynamic token axis. Keep the prompt-encoding host path and
classify_decoder_outputs framework-independent. ONNX uses fpn_from_runtime and
prepare_decoder_tensors_bridged; MXQ uses fpn_from_runtime plus the detected decoder builder.
eval_sav works unchanged for both.
Load the optional runtime before downloading artifacts; validate explicit artifact paths
(prompt weights included) with FileNotFoundError before any download; dispose a backend that
fails after create() inside its builder (the caller assigns it only on success) while
suppressing dispose failures so they cannot mask the original error; treat -1 in ONNX graph
validation as "must be dynamic", not a wildcard; validate point labels as
exactly 1/0; build host prompt tensors on the weights' device so device="cuda" works; and
reject single-artifact path options in every CLI command that builds the engine, not just
predict.
- Never depend on the PyPI `sam2` package (unofficial third-party mirror) or a manually cloned
facebookresearch/sam2 checkout. mask_generation's host-side prompt encoding is a from-scratch
port verified bit-for-bit against the real predictor, backed by a small Hub-hosted weights
bundle, not package data.
- mask_generation validates on SA-V val via datasets/sa-v.yaml. SA-V is NOT auto-downloaded and
must never be mirrored on Mobilint infrastructure: Meta gates it behind a download form, so the
user supplies the official sav_val.tar (or its extracted directory) with
--annotation-dir/--image-dir, like Cityscapes. Not sha256-pinned (user-supplied, not fetched);
identity comes from the readiness inventory. CC BY 4.0 by Meta AI. Readiness pins all three
inventory counts (155 videos / 293 masklets / 31967 masks); video and masklet totals alone
accept a truncated source. Both the organizer and readiness require every non-zero mask value
to be one object ID, since `{1, 2}` survives a unique-value count but `> 0` binarization makes
it all foreground. Readiness validates every mask (not just the first per video); bilevel masks
are validated from the header, since a 1-bit PNG cannot hold a non-zero background. Reject
`sav_val.txt` ids that fail SAV_VIDEO_ID_PATTERN or escape staging, before any write. Registering a new dataset requires readiness (`_*_ready` + `dataset_ready`
- Registering a new dataset requires readiness (`_*_ready` + `dataset_ready`
map) before the organizer, since staged validation calls `dataset_ready`; also register the
organizer in the test_dataset_organizer.py parametrize lists.

Expand Down
124 changes: 6 additions & 118 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,118 +142,12 @@ The current ownership boundary is deliberate:
where every other face model uses `0.7`. Do not normalize the exception away; a size change here is a
durable model-behavior change requiring the guide, both skill copies, and
`mblt_vision/README.md` to be updated in the same commit.
- `eval_sav` requires already-binarized candidate masks, enumerated per dtype (bool, integer
`{0, 1}`/`{0, 255}`, float `{0.0, 1.0}`). A weaker "single positive value" rule still admits a
probability map such as `{0.0, 0.5}`, or a uniform `0.5` candidate that `astype(bool)` turns
entirely into foreground. This is deliberately stricter than the SA-V ground-truth mask check,
where any single positive value is a legitimate object ID.
- For NYU Depth evaluation, rank results by delta1 and retain abs_rel then
RMSE (m) as secondary metrics. Median-align each image and average every
metric per image, following Ultralytics' depth-validation convention.
- `mask_generation` (`SAM2HieraLarge`) is the precedent for a promptable, multi-artifact model:
it loads two backend instances (encoder + decoder) and takes point prompts, so it
bypasses `MBLT_Engine.__init__`, `build_preprocess`/`build_postprocess`, and
`create_model_class`'s single-artifact legacy constructor entirely, implementing its own
`preprocess`/`predict`/`predict_preprocessed` instead. Its `models/SAM2HieraLarge.yaml` is
documentation/test-metadata only, not actually loaded. Reuse `wrapper.download_hub_artifact`
(extracted from `MBLT_Engine._download_hub_artifact`) for any future model needing more than
one Hub artifact, rather than duplicating Hub-resolution logic.
- `mask_generation` supports both frameworks with `MBLT_Engine`-style semantics:
`framework="mxq"` (default, two `MobilintNPUBackend` MXQs from the board folder) or
`framework="onnx"` (two `ONNXBackend` sessions over the same-stem
`sam2_hiera_large_{encoder,decoder}.onnx` exports at the Hub repo root, board-agnostic).
Framework is inferred from explicit `encoder_onnx_path`/`decoder_onnx_path` vs
`encoder_mxq_path`/`decoder_mxq_path` suffixes and conflicts fail fast; NPU-only arguments are
ignored for ONNX. The runtimes have different graph contracts, pinned in
`_sam2_contracts.py` and validated at construction. Two MXQ decoder generations exist and
both are supported, identified from the loaded artifact's declared input shapes
(`detect_decoder_contract`, run while the engine is built so a drifted artifact fails before
any inference is spent): the *assembled* contract takes six host-flattened positional inputs
(tokens pre-concatenated, `image_embeddings + dense` pre-summed; four outputs), while the
*bridged* contract -- the SDK tutorial's legacy-parser decoder, whose MBLT carries that
assembly as an in-graph host-bridge subgraph -- takes the prompt encoder's raw outputs
(two outputs: `classify_decoder_outputs` keeps `masks`/`iou` required and treats
`sam_tokens`/`object_score` as optional). The uploaded ONNX graphs use the direct-MBLT
boundary: batched NHWC encoder input `input_image_0`, NHWC FPN outputs, and six named
decoder inputs with a dynamic prompt axis. ONNX supplies the raw prompt tensors by name;
MXQ supplies the same semantic tensors positionally. The ONNX
pipeline is numerically verified against the official `facebookresearch/sam2` fp32 predictor
(identical binary masks; opt-in `tests/test_mask_generation_onnx.py` covers it end-to-end
without NPU hardware); the bridged MXQ contract is numerically verified on real hardware by
the opt-in `test_sam2_bridged_contract_predicts_numerically` (ground-truth mask IoU across
1-3-point prompts, artifacts supplied via `MBLT_VISION_SAM2_BRIDGED_{ENCODER,DECODER}_MXQ`),
and `eval_sav` works unchanged for both frameworks. Resolve the optional runtime before
downloading any artifact, so a missing `onnxruntime` reports the package extra rather than a
network failure. Build each backend so it disposes itself when `launch()`/graph validation
fails after `create()`: the caller only assigns it on success, so the constructor's cleanup
cannot otherwise reach it. Suppress disposal failures while unwinding (both in those builders
and in the constructor, which uses `_close(suppress_errors=True)` like the base engine) so a
failing `dispose()` cannot replace the original construction error. Validate every explicitly
supplied artifact path -- prompt weights included -- with a fail-fast `FileNotFoundError` after
the argument-coherence checks but before any download, and normalize the MXQ `target_device`
there too so an unknown board reports as such rather than as a Hub failure. Lowercase an
explicit `framework` before validating it, matching `_model_paths.resolve_framework`, so
`framework="ONNX"` behaves as it does for every other model. Mask-generation-only CLI overrides
(`--encoder-*-path`, `--decoder-*-path`, `--prompt-weights-path`) must be rejected by every
command that builds a non-mask engine, since the generic engine never receives them and would
silently run the downloaded default instead. ONNX graph validation treats `-1` as
"this axis must be declared dynamic" (ONNX Runtime reports a dynamic axis as a `str`), not as a
wildcard: a decoder frozen at one token count would otherwise pass construction and fail inside
ONNX Runtime for two- and three-point prompts. Point prompts are validated as 1-3 points with finite
coordinates and labels of exactly `1`/`0` (checked before the integer cast, which would
otherwise truncate `0.5` into a valid label) before any backend call, and host prompt tensors
are built on the weights' device so `device="cuda"` works. `original_hw` is likewise validated
as exactly two positive whole numbers, and the source image as numeric and finite, before
either backend runs: interpolation preserves NaN, and a zero dimension yields infinite prompt
coordinates. Both FPN converters pin each level's complete `(C, H, W)` rather than only its
channel count, since `build_backbone_features` `view()`s them and a same-element-count geometry
would be silently rearranged into a corrupted FPN that still passes every downstream check. `classify_decoder_outputs` requires
exactly three mask candidates and rejects non-finite decoder outputs, so a NaN cannot reach
`argmax` over the IoU scores or the `> 0` mask threshold. Mask generation models reject `--model-path`/`--mxq-path`/`--onnx-path`
in every CLI command that builds one, never only in `predict`.
- Never add the PyPI `sam2` package as a dependency of `mblt_vision` (it is an unofficial
third-party mirror, not Meta's) and never require a manually cloned
`facebookresearch/sam2` checkout. Host-side prompt encoding is instead a from-scratch,
dependency-free port of the official prompt encoder/mask-decoder token setup in
`mask_generation/_sam2_prompt.py` and `_sam2_host.py`, backed by a small (~16KB) bundle of
weight tensors extracted from the official checkpoint and hosted at
`mobilint/sam2-hiera-large`'s Hub repo root (`sam2_hiera_large_prompt_weights.pt`) --
downloaded the same way as the encoder/decoder MXQ artifacts, not shipped as package data.
Any change to that port must stay numerically verified against the real
`facebookresearch/sam2` predictor (`tests/test_mask_generation_prompt_encoding.py`, opt-in,
skips without a real `sam2` install). No added dependency: the input resize/normalize step
is plain `torch` (`F.interpolate` bilinear with `antialias=True` reproduces torchvision's
tensor `Resize` bit-for-bit), so mask_generation runs on the package's existing dependencies.
- `mask_generation` evaluates on the SA-V validation split (155 videos, 293 masklets, 31967
annotated masks; JPEG frames + per-object binary PNG masks, binarized as `> 0`). The registry
entry `datasets/sa-v.yaml` does not download it: Meta distributes SA-V through a form-gated
portal, and it must never be mirrored on Mobilint infrastructure. Users supply the official
`sav_val.tar` (or its extracted directory) via `--annotation-dir`/`--image-dir`, exactly as
Cityscapes requires its manual archives; `_resolve_sav_source` fails with the portal link and
the SAM 2 `sav_dataset/README.md` layout reference rather than falling back to a URL. The
archive is deliberately absent from `PINNED_ARCHIVE_SHA256` because it is user-supplied rather
than fetched from a URL this package controls, so identity is enforced on content by the
readiness inventory instead. SA-V is CC BY 4.0 by Meta AI. The
organizer keeps only annotated frames. Readiness pins all three counts
(`SAV_VALIDATION_MASK_COUNT` alongside the video/masklet counts), since video and masklet totals
alone accept a source truncated to a few annotated frames per masklet and would silently
evaluate a different corpus. Both the organizer's staged-mask check and the readiness check
require every non-zero mask value to be one object ID: counting unique values alone accepts a
`{1, 2}` mask that `> 0` binarization turns entirely into foreground. Readiness checks geometry
and values for *every* cached mask, not the first per video. The official split is 1-bit
bilevel, a format that cannot encode a non-zero background, so those masks are validated from
the header and only non-bilevel masks are decoded -- complete validation at header cost
(~3s for 31967 masks; decoding them all would be ~380s on every `val`).
- SA-V video ids come from `sav_val.txt` file contents rather than a directory listing, so
`construct_sav` must reject ids failing `SAV_VIDEO_ID_PATTERN` and must confirm every staged
path resolves inside the staging tree, both *before* any `makedirs`/`copy`. COCO applies the
equivalent guard to its JSON-declared file names; organizers that build paths only from
directory listings do not need it. The evaluation protocol (`eval_sav`) is ported from the
validated `sam2-mxq-pipeline` reference: seed-deterministic area-balanced sampling, synthetic
point prompts from the GT mask (distance-transform peak, dilated-mask negatives), and
own-selection mean IoU as the primary metric with best-of-3 secondary. Numbers measured on
SA-V val are a different protocol from that reference's sav_train-sampled 0.7757 and are not
directly comparable.
- Reuse `wrapper.download_hub_artifact` (extracted from `MBLT_Engine._download_hub_artifact`) for
any future model needing more than one Hub artifact, rather than duplicating Hub-resolution
logic.
Comment thread
parkjinman98 marked this conversation as resolved.

## Python-First Architecture

Expand All @@ -274,14 +168,8 @@ The current ownership boundary is deliberate:

## Benchmark and Compilation Tooling

- The unified benchmark runner's `TASK_CHOICES` is the subset of `VISION_TASKS` it can actually
execute. `mask_generation` is excluded because `_run_target` builds a generic `MBLT_Engine` and
`_evaluate` has no `eval_sav` branch; benchmark it with `mblt-vision val` instead. Do not wire a
new canonical task into the runner's choices before its engine and evaluator paths exist.
- `classify_decoder_outputs` pins the decoder mask layout to `(N, 65536)` or `(N, 256, 256)`,
optionally batched, before reshaping. A channels-last `(1, 256, 256, 3)` output has a matching
element count and candidate count, so only the layout check catches it; without it the reshape
interleaves the candidates into plausible but corrupted masks.
- The unified benchmark runner's `TASK_CHOICES` must only list tasks it can actually execute. Do not
wire a new canonical task into the runner's choices before its engine and evaluator paths exist.
- Keep executable benchmark organizers, the unified benchmark runner, and result comparison scripts
directly under `benchmark/`. Put reusable benchmark reporting helpers in `mblt_vision.benchmark`.
- Keep executable compilation helpers and their guide directly under `compile/`. Do not recreate a
Expand Down Expand Up @@ -355,7 +243,7 @@ The current ownership boundary is deliberate:
candidate tensors inside `non_max_suppression` is 1.8x in numpy and measured ~2x
*slower* here, because six boolean index operations per iteration cost more in torch
than one gather through a shrinking index; that measurement is recorded in the function.
- Every `eval_*` evaluator except `eval_sav` runs through
- Every `eval_*` evaluator runs through
`utils/evaluation/_pipeline.map_batched_inference`, which prepares batches and runs
postprocessing on two separate bounded thread pools while inference stays on the consuming
thread, following `mblt-model-ops`'s helper of the same name. Results come back in input
Expand Down
28 changes: 3 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,7 @@ pip install "mblt-vision-python[onnxruntime-gpu]"

## Quick start

Most models include matching `preprocess`/`postprocess` behavior around a raw
call (mask generation is the exception -- see below):
Each model includes its matching preprocess and postprocess behavior:

```python
from mblt_vision import ResNet50
Expand Down Expand Up @@ -71,20 +70,6 @@ code should use the task subpackages (for example,

`obb` is the canonical oriented-bounding-box task name.

Mask generation is promptable, so `SAM2HieraLarge` takes point prompts and
returns candidate masks from a single `predict()` call instead of the separate
`preprocess`/raw-call/`postprocess` steps above:

```python
from mblt_vision.mask_generation import SAM2HieraLarge

model = SAM2HieraLarge() # framework="onnx" for ONNX Runtime inference
result = model.predict("image.jpg", points=[[320, 240]], labels=[1])
```

See the [mask generation section](mblt_vision/README.md#mask-generation) for the
prompt contract, the two-artifact layout, and SA-V validation.

## Model Zoo migration

Vision is now maintained in this package. `mblt-model-zoo` retains
Expand All @@ -102,24 +87,17 @@ mblt-vision predict --source image.jpg --model resnet50
```

`predict` is the single inference command for classification, depth estimation,
object and face detection, instance and semantic segmentation, OBB, pose
estimation, and point-prompted mask generation. The selected model determines
its task and processing pipeline.
object and face detection, instance and semantic segmentation, OBB, and pose
estimation. The selected model determines its task and processing pipeline.
By default it downloads the model artifact and saves a plotted result under
`runs/vision/predict/`. Use `--output` to choose the result-image path,
`--topk` for classification labels, and `--conf-thres`/`--iou-thres` for
detection-style tasks. `--framework onnx` selects ONNX Runtime inference;
`--target-device` and `--core-mode` select the MXQ board/runtime mode.

Mask generation models require 1-3 point prompts (`--point X,Y,LABEL`, where
LABEL is 1 for positive and 0 for negative) and load two artifacts, overridden
with `--encoder-mxq-path`/`--decoder-mxq-path` (or the `--encoder-onnx-path`/
`--decoder-onnx-path` pair) rather than `--model-path`/`--mxq-path`/`--onnx-path`.

```bash
mblt-vision predict --source image.jpg --model yolo11m --conf-thres 0.4 --output result.jpg
mblt-vision predict --source image.jpg --model yolo11m-pose --target-device regulus-ra --core-mode single
mblt-vision predict --source image.jpg --model sam2-hiera-large --point 320,240,1
```

The corresponding `mblt-model-zoo` commands use the same standalone handlers for
Expand Down
Loading
Loading