| --- |
| license: other |
| license_name: sam3-license |
| license_link: https://huggingface.co/facebook/sam3 |
| pipeline_tag: image-segmentation |
| library_name: onnxruntime |
| tags: |
| - sam3 |
| - segmentation |
| - onnx |
| - text-prompted |
| - open-vocabulary |
| - instance-segmentation |
| - fp16 |
| --- |
| |
| # SAM3 β Split Encoder / Decoder (ONNX) |
|
|
| Text-prompted, open-vocabulary image segmentation. Give it an image and a text concept |
| (e.g. `"person"`, `"red car"`) and it returns instance masks, boxes and scores for that concept. |
|
|
| This is the **two-stage split** of SAM3: |
|
|
| | File | Role | Runs | |
| |------|------|------| |
| | `sam3_encoder.onnx` | Vision backbone: `image β 3 feature tensors` | **once per image** | |
| | `sam3_decoder.onnx` | Detection head: `features + text β masks/boxes/scores` | **once per prompt** | |
|
|
| Because the backbone is separated out, you encode the image **once** and then decode |
| **many prompts** cheaply against the cached features β instead of re-running the whole model |
| for every prompt. |
|
|
| **Precision:** the ONNX graph stores weights in **fp32** (with a few int8-quantized tensors) |
| and has **fp32** inputs/outputs (feed a normal `float32` image, read `float32` logits/boxes/masks). |
| It is meant to run at **fp16 on GPU** β build/enable fp16 in your runtime (e.g. TensorRT). The |
| latency and VRAM figures below are for that fp16 GPU deployment. |
|
|
| --- |
|
|
| ## Inputs & outputs |
|
|
| **Encoder** β `sam3_encoder.onnx` |
|
|
| | Tensor | Shape | dtype | Notes | |
| |--------|-------|-------|-------| |
| | `image` (in) | `[1, 3, 924, 924]` | float32 | RGB, CHW, normalized `(x/255 - 0.5) / 0.5` | |
| | `/Add_179_output_0` (out) | `[1, 4356, 256]` | float32 | feature β pass straight to decoder | |
| | `/fused_229/Conv_output_0` (out) | `[1, 256, 264, 264]` | float32 | feature β pass straight to decoder | |
| | `/fused_232/Conv_output_0` (out) | `[1, 256, 132, 132]` | float32 | feature β pass straight to decoder | |
|
|
| **Decoder** β `sam3_decoder.onnx` |
|
|
| | Tensor | Shape | dtype | Notes | |
| |--------|-------|-------|-------| |
| | *3 feature tensors* (in) | *(above)* | float32 | fed by name from the encoder outputs | |
| | `tokenized_text` (in) | `[1, 32]` | int64 | CLIP-style `input_ids`, zero-padded to 32 | |
| | `pred_masks` (out) | `[1, 200, 264, 264]` | float32 | per-query mask logits | |
| | `pred_logits` (out) | `[1, 200, 1]` | float32 | per-query class logit | |
| | `pred_boxes` (out) | `[1, 200, 4]` | float32 | per-query box, normalized `cxcywh` | |
| | `presence_logit_dec` (out) | `[1, 1]` | float32 | global concept-presence logit | |
|
|
| The encoder's output tensor names are **identical** to the decoder's feature input names, so |
| features pass through by name β no remapping needed. |
|
|
| --- |
|
|
| ## How to use |
|
|
| Requires: `onnxruntime-gpu` (or `onnxruntime`), `opencv-python`, `numpy`, `transformers`. |
| The tokenizer comes from [`facebook/sam3`](https://huggingface.co/facebook/sam3). |
|
|
| ```python |
| import cv2 |
| import numpy as np |
| import onnxruntime as ort |
| from transformers import AutoTokenizer |
| |
| INPUT_SIZE = 924 |
| TEXT_LEN = 32 |
| SCORE_THRESHOLD = 0.25 |
| |
| # ββ load once ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ |
| providers = ["CUDAExecutionProvider", "CPUExecutionProvider"] |
| encoder = ort.InferenceSession("sam3_encoder.onnx", providers=providers) |
| decoder = ort.InferenceSession("sam3_decoder.onnx", providers=providers) |
| |
| tokenizer = AutoTokenizer.from_pretrained("facebook/sam3") |
| tokenizer.pad_token_id = 0 # CLIP-style zero padding |
| |
| enc_out_names = [o.name for o in encoder.get_outputs()] # the 3 feature tensors |
| dec_in_names = [i.name for i in decoder.get_inputs()] |
| feature_names = [n for n in dec_in_names if n != "tokenized_text"] |
| |
| |
| # ββ preprocessing βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ |
| def preprocess_image(frame_rgb): |
| """HxWx3 uint8 RGB -> [1,3,924,924] float32.""" |
| resized = cv2.resize(frame_rgb, (INPUT_SIZE, INPUT_SIZE), interpolation=cv2.INTER_LINEAR) |
| img = resized.astype(np.float32) / 255.0 |
| img = (img - 0.5) / 0.5 |
| img = img.transpose(2, 0, 1)[np.newaxis] |
| return np.ascontiguousarray(img) |
| |
| def tokenize(prompt): |
| ids = tokenizer(prompt, padding="max_length", max_length=TEXT_LEN, |
| truncation=True, return_tensors="np")["input_ids"] |
| return ids.astype(np.int64) |
| |
| |
| # ββ postprocessing ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ |
| def sigmoid(x): |
| return 1.0 / (1.0 + np.exp(-np.clip(x, -50, 50))) |
| |
| def cxcywh_to_xyxy(b): |
| cx, cy, w, h = b[..., 0], b[..., 1], b[..., 2], b[..., 3] |
| return np.stack([cx - 0.5 * w, cy - 0.5 * h, cx + 0.5 * w, cy + 0.5 * h], axis=-1) |
| |
| def postprocess(out, orig_hw): |
| H, W = orig_hw |
| scores = sigmoid(out["pred_logits"][0, :, 0]) * sigmoid(out["presence_logit_dec"][0]) |
| keep = scores > SCORE_THRESHOLD |
| boxes = cxcywh_to_xyxy(out["pred_boxes"][0][keep]) * np.array([W, H, W, H], np.float32) |
| dets = [] |
| for score, box, m in zip(scores[keep], boxes, out["pred_masks"][0][keep]): |
| mask = sigmoid(cv2.resize(m, (W, H), interpolation=cv2.INTER_LINEAR)) > 0.5 |
| dets.append({"score": float(score), "box_xyxy": box, "mask": mask}) |
| return dets |
| |
| |
| # ββ inference: encode ONCE, decode MANY prompts βββββββββββββββββββββββββββββββ |
| frame_bgr = cv2.imread("image.jpg") |
| frame_rgb = cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2RGB) |
| orig_hw = frame_rgb.shape[:2] |
| |
| # Stage 1 β backbone (once) |
| feats = encoder.run(enc_out_names, {"image": preprocess_image(frame_rgb)}) |
| feat_feed = dict(zip(enc_out_names, feats)) |
| |
| # Stage 2 β decode each prompt against the cached features |
| for prompt in ["person", "car", "dog", "traffic light"]: |
| feed = {n: feat_feed[n] for n in feature_names} |
| feed["tokenized_text"] = tokenize(prompt) |
| outs = decoder.run(None, feed) |
| out = dict(zip([o.name for o in decoder.get_outputs()], outs)) |
| dets = postprocess(out, orig_hw) |
| print(f"{prompt}: {len(dets)} detection(s)") |
| ``` |
|
|
| **Scoring recap:** `score = sigmoid(pred_logits) * sigmoid(presence_logit_dec)`, keep `score > 0.25` |
| (tune to taste). Boxes are `cxcywh` normalized β convert to `xyxy` and scale by `[W, H, W, H]`. |
| Masks are logits at `264Γ264` β resize to source size, `sigmoid(...) > 0.5`. |
|
|
| --- |
|
|
| ## Benchmarks |
|
|
| - **NVIDIA RTX 4070 Ti β DeepStream (TensorRT, fp16):** **~60 ms** end-to-end for **1 image + 4 prompts** |
| (one encoder pass + four decoder passes). |
|
|
| - **VRAM:** ~**1800 MB** with both encoder and decoder sessions loaded. |
|
|
| Because the encoder runs once and decoders are cheap, added prompts cost far less than a full |
| model pass each β the per-prompt cost is dominated by the small decoder, not the backbone. |
|
|
| To reproduce on your own hardware, run the encoder once per image and the decoder per prompt |
| (as in the example above), and time each stage separately (encoder vs. decoder). |
|
|
| --- |
|
|
| ## Notes |
|
|
| - **Precision:** fp32 weights (a few int8-quantized tensors) with fp32 I/O; intended to run at |
| **fp16 on GPU**. Enable fp16 in your runtime (e.g. TensorRT) for the benchmarked latency/VRAM. |
| Running the raw fp32 graph (e.g. plain ONNX Runtime) is correct but slower and uses more memory. |
| - **ONNX opset 17** (IR version 8) β use a matching `onnxruntime` (β₯ 1.14). |
| - Fixed input resolution **924Γ924** β the position embeddings are baked to this grid; resizing |
| the input is not supported. |
| - Single execution context per session: don't run one session from multiple threads at once; |
| serialize calls or use one session per thread. |
|
|
| ## License & attribution |
|
|
| Derived from [`facebook/sam3`](https://huggingface.co/facebook/sam3). Use of these weights is |
| subject to the SAM3 license β review and accept it before use. |
|
|