AgentCanvas / Pages / Developer Guide / Nodesets / Model / SAM NodeSet
2026-07-05

The SAM NodeSet (SamNodeSet, workspace/nodesets/model/model_sam.py) wraps the Segment Anything family — SAM 1, SAM 2.1, and SAM 3 as real backends — into pure single-step segmentation primitives: point/box/auto/text segmentation plus an image-embedding node. Rewritten 2026-07-05 to the FM-nodeset template: variant and checkpoint are node config (swapping them never changes the graph — every variant returns the same masks envelope), and the server is fully stateless: no embedding cache, no sessions. Everything procedural — iterative refinement, embedding reuse, cross-model composition — lives in the graph, not in the nodeset. SAM 2.1/3 are served through the resident transformers (≥ 5.13 ships Sam2Model/Sam3Model), so the whole family costs zero extra dependencies.

env: ac-fm

Primitive Purpose Variants
model_sam__segment_points Point-prompted segmentation; optional mask_input closes an outer-loop refinement cycle. sam1 | sam2
model_sam__segment_box Box-prompted segmentation — a single [x1,y1,x2,y2] box or a batch (one candidate per box). sam1 | sam2
model_sam__segment_auto Full-scene segmentation with a point grid; knobs fully in config. sam1 | sam2
model_sam__segment_text Concept/text-prompted instance segmentation — SAM 3's native capability. sam3
model_sam__embed_image Image-encoder features as an embedding envelope — the encode-once entry for graph-level reuse. sam1

Embedding in/out (the envelope ports) is sam1-only for now: SAM 2/3 image features are multi-level, which the single-tensor envelope cannot carry (an envelope v2 is a separate decision). Prompted nodes configured to sam2 emit an empty image_embedding and raise on envelope injection.


1. Canvas Nodes

model_sam__segment_points

Field Detail
Inputs image (TEXT — raw base64 PNG or an embedding envelope, sniffed automatically; one required port instead of two optional ones because optional ports do not gate firing), points (TEXT — JSON [[x,y],…]), labels (TEXT — JSON [1|0,…]), mask_input (TEXT, optional — a candidate's low_res_logits_b64 from a previous round)
Outputs masks (TEXT — §2 masks envelope, candidates score-sorted), image_embedding (TEXT — §2 embedding envelope used for this call)
Config variant (select sam1|sam2), ckpt (text — blank = per-variant default, §4), multimask (toggle — 3 ranked candidates vs single best)
Backend call sam1: fresh SamPredictor per call → predict(point_coords, point_labels, mask_input?, multimask_output). sam2: Sam2ProcessorSam2Model.forward (a fed-back mask_input becomes input_masks) → post_process_masks

Iterative refinement belongs to the outer loop — the node is a pure single-step forward. Each candidate carries low_res_logits_b64 (256×256 float32 ≈ 256 KB); wiring one back into mask_input closes the refinement cycle at graph level, keeping pure dataflow and episode replayability.

model_sam__segment_box

Field Detail
Inputs image (TEXT — raw base64 PNG or embedding envelope), boxes (TEXT — JSON [x1,y1,x2,y2] or a list of such boxes)
Outputs masks (one candidate per box), image_embedding
Config variant (select sam1|sam2), ckpt
Backend call sam1: per-box predict(box=…, multimask_output=False) on one shared image state. sam2: batched input_boxes through one Sam2Model.forward

model_sam__segment_auto

Field Detail
Inputs image_b64 (TEXT — original image only; the generators re-encode per crop internally, so they cannot consume a precomputed embedding)
Outputs masks (area-sorted; no logits / embedding outputs)
Config variant (select sam1|sam2), ckpt, points_per_side (slider 8–64), pred_iou_thresh (0.86), stability_score_thresh (0.92), min_mask_area (100), crop_n_layers (slider 0–3, sam1 only — see deviations below)
Backend call sam1: a fresh SamAutomaticMaskGenerator per call, knobs fully from config — the pre-rewrite code mutated a shared generator's points_per_side after construction, which the generator never reads (the point grid is built in __init__), so that knob was silently a no-op. sam2: the HF mask-generation pipeline (kwargs filtered against its signatures)

Deviations on the sam2 path: the HF pipeline does not expose per-candidate stability_score (the key is simply absent from sam2 auto candidates), min_mask_area is applied as a local post-filter, and crop_n_layers > 0 raises — the transformers 5.13 pipeline crashes stacking unequal-sized crops (upstream bug), and a clear node error beats silently ignoring the knob. Use sam1 for cropped auto-segmentation.

model_sam__segment_text

Field Detail
Inputs image_b64 (TEXT), text (TEXT — a short noun-phrase concept, e.g. "red circle")
Outputs masks (score-sorted, one candidate per matching instance; iou_score carries the instance score)
Config variant (select, sam3), ckpt, score_thresh (0.5 — instance filter), mask_threshold (0.5 — per-pixel binarization)
Backend call Sam3Processor(images, text)Sam3Model.forwardpost_process_instance_segmentation (with explicit target_sizes — the processor output carries no original sizes, and without it masks come back at model resolution)

This is SAM 3's native concept prompting — no detector in front. The weights are HF-gated (facebook/sam3, manual approval on the model page); until access is granted the engine latches degraded (§5). The graph-level GDINO → __segment_box composition remains the text route for the sam1/sam2 variants.

model_sam__embed_image

Field Detail
Inputs image_b64 (TEXT)
Outputs image_embedding (TEXT — full envelope on the wire)
Config variant (select, sam1 only — SAM 2/3 features are multi-level; see the envelope note in the intro), ckpt

Reuse is a graph-level decision. SAM's structure is a heavy image encoder + light prompt decoder; the wrapper exposes that split as ports instead of a server-side cache. A graph that prompts the same frame repeatedly runs __embed_image once (or catches any prompted node's image_embedding output), stashes the envelope in a state container, and feeds it back into the image port — the encoder is skipped, and the server stays a pure function. SAM 1's envelope is ≈ 5.6 MB of TEXT (1×256×64×64 float32, base64), which the msgpack transport handles comfortably.


2. Envelopes

All segmentation nodes return the same masks envelope regardless of variant — swapping checkpoints never changes the graph:

{"masks": [{"mask_index": 0,
            "mask_b64": "<base64 PNG, 0/255 grayscale>",
            "bbox_xyxy": [201, 121, 439, 359],
            "iou_score": 0.9772,
            "area": 44945,
            "low_res_logits_b64": "<base64 of 256x256 float32 buffer>"}],
 "count": 3, "image_w": 640, "image_h": 480}

low_res_logits_b64 appears on prompted candidates only (__segment_auto discards logits); sam1 auto candidates carry stability_score instead (the sam2 pipeline does not expose it); __segment_text candidates reuse iou_score for the SAM 3 instance score. The mask_b64 key name is load-bearing: aoplanner's _union_sam_masks parses it.

The embedding envelope is self-describing — a byte-exact float32 buffer plus the predictor sizes and the provenance needed to reject a mismatched injection:

{"b64": "<base64 of the C-contiguous float32 buffer>",
 "shape": [1, 256, 64, 64], "dtype": "float32",
 "original_hw": [480, 640], "input_hw": [768, 1024],
 "variant": "sam1", "ckpt_id": "sam_vit_b.pth"}

Injecting an envelope whose variant/ckpt_id mismatches the target node's config raises — a node error beats a silently wrong mask. Injection round-trips byte-identically: masks produced from an injected envelope equal the direct-image path exactly (verified through the server transport as well).


3. Server Mode

parallelism = "shared"; server_python = conda_env_python("ac-fm", "SAM_PYTHON") since 2026-07-05 (segment-anything is lower-bound-only → shared-env admission; installed with opencv-python-headless by scripts/install/install_ac_fm.sh). One engine class per variant (_Sam1Engine/_Sam2Engine/_Sam3Engine) behind a lazy registry keyed (variant, ckpt) — several checkpoints coexist in one server — and engines hold nothing but loaded weights: no cache, no sessions, every call builds a fresh predictor. The GPU section is single-flight per engine to bound VRAM under concurrent eval workers. SAM 2.1/3 use the transformers implementations, not the facebookresearch packages (nothing new to install; the PyPI sam2/sam3 names have uncertain provenance). Loading the sam2.1 checkpoint logs a benign sam2_video-vs-sam2 config-type warning — the image path shares a weight subset, expected per HF.

Verification: the sam1 rewrite is byte-faithful — on the old hosting env, point/box/auto outputs are byte-identical to the pre-rewrite code (auto compared at its effective old grid, see the no-op knob note in §1). Serving from ac-fm shifts torch 2.5.1 → 2.8.0, whose conv kernels drift mask boundaries by ±1 px and IoU scores at the third decimal — accepted, since no graph or test consumed the old outputs. The sam2/sam3 variants have no byte baseline (different implementation lineage), so they gate on synthetic-image analytic-GT IoU ≥ 0.9 (point/box/auto/text all ≥ 0.97 at verification, matching a 0.99 sam1 calibration), determinism, and a logits→mask_input round-trip. Load: POST /api/components/nodesets/model_sam/load?mode=server.


4. Environment Variables

Variable Default Purpose
SAM_PYTHON ac-fm env python Interpreter for the server subprocess
SAM_DEVICE cuda:0 Inference device

The former SAM_VERSION / SAM_MODEL_CFG env vars are gone — variant and checkpoint are node config (env panels carry env-side runtime knobs only; model identity belongs on the node). Per-variant checkpoint defaults when ckpt is blank:

Variant Default checkpoint Notes
sam1 data/habitat/checkpoints/sam/sam_vit_b.pth Anchored to the repo root by an upward walk (the auto_host subprocess cwd is not the repo root); relative overrides are anchored the same way; model type (vit_b/vit_l/vit_h) inferred from the filename
sam2 facebook/sam2.1-hiera-base-plus HF repo id (ungated, ~0.3 GB) — repo ids are never path-anchored; a local directory also works
sam3 facebook/sam3 HF repo id, gated (manual approval, ~3.4 GB); without access the engine latches degraded

5. Degraded Mode

An engine load failure latches (no retry storm) and every call on that engine returns empty outputs (masks = "", image_embedding = "") with a degraded self-log entry. Input-contract violations — malformed JSON, an embedding envelope for the wrong variant/ckpt — raise instead: the executor records a node error and the eval layer convicts the episode.


6. Composition & Consumers

AgentCanvas docs