Instructions to use ArAnirudh/slimsam-77-uniform-boxes with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers.js
How to use ArAnirudh/slimsam-77-uniform-boxes with Transformers.js:
// npm i @huggingface/transformers import { pipeline } from '@huggingface/transformers'; // Allocate pipeline const pipe = await pipeline('mask-generation', 'ArAnirudh/slimsam-77-uniform-boxes');
File size: 6,881 Bytes
6a74f50 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 | """Export SlimSAM's prompt encoder + mask decoder to ONNX with BOX prompts.
The browser's SlimSAM (`Xenova/slimsam-77-uniform`) decoder declares no
`input_boxes`, and transformers.js drops inputs a session does not declare, so
every box the editor sent was silently ignored. This export takes boxes the way
Meta's own SAM ONNX export does: a box is two extra points labelled 2 (top-left
corner) and 3 (bottom-right corner). Inputs and outputs keep transformers.js's
names, so `SamModel` drives it unchanged.
One difference from HF's traced graph: a prompt with no box is NOT padded
inside the graph. The caller appends the "not a point" pad (0, 0, label -1),
exactly what HF's encoder does internally when no box is given; a prompt with a
box has no pad, as in HF.
Usage (onnx, onnxruntime, onnxscript importable):
python scripts/slimsam-onnx/export_decoder.py <out_dir>
writes <out_dir>/onnx/prompt_encoder_mask_decoder{,_quantized}.onnx and checks
both against PyTorch SamModel on points-only and box prompts.
"""
from __future__ import annotations
import os
import sys
import numpy as np
import torch
from transformers import SamModel
MODEL_ID = "nielsr/slimsam-77-uniform"
class PromptDecoder(torch.nn.Module):
def __init__(self, sam: SamModel):
super().__init__()
self.pe = sam.prompt_encoder
self.md = sam.mask_decoder
def forward(self, input_points, input_labels, image_embeddings, image_positional_embeddings):
pe = self.pe
# HF _embed_points / _embed_boxes, with box corners as labels 2 and 3.
size = (pe.input_image_size, pe.input_image_size)
emb = pe.shared_embedding(input_points + 0.5, size)
lab = input_labels[..., None]
emb = torch.where(lab == -1, pe.not_a_point_embed.weight, emb)
emb = torch.where(lab != -10, emb, torch.zeros_like(emb))
for k in range(4):
emb = torch.where(lab == k, emb + pe.point_embed[k].weight[None, None, :, :], emb)
dense = pe.no_mask_embed.weight.reshape(1, -1, 1, 1).expand(
image_embeddings.shape[0], -1, pe.image_embedding_size[0], pe.image_embedding_size[1])
masks, iou = self.md(
image_embeddings=image_embeddings,
image_positional_embeddings=image_positional_embeddings,
sparse_prompt_embeddings=emb,
dense_prompt_embeddings=dense,
multimask_output=True,
)
return iou, masks
def main(out_dir: str) -> None:
import onnxruntime as ort
from onnxruntime.quantization import QuantType, quantize_dynamic
os.makedirs(os.path.join(out_dir, "onnx"), exist_ok=True)
fp32 = os.path.join(out_dir, "onnx", "prompt_encoder_mask_decoder.onnx")
q8 = os.path.join(out_dir, "onnx", "prompt_encoder_mask_decoder_quantized.onnx")
sam = SamModel.from_pretrained(MODEL_ID).eval()
dec = PromptDecoder(sam).eval()
torch.manual_seed(0)
emb = torch.randn(1, 256, 64, 64)
pos = sam.get_image_wide_positional_embeddings().detach()
pts = torch.tensor([[[[400.0, 300.0], [0.0, 0.0]]]])
lab = torch.tensor([[[1, -1]]], dtype=torch.int64)
torch.onnx.export(
dec, (pts, lab, emb, pos), fp32,
input_names=["input_points", "input_labels", "image_embeddings", "image_positional_embeddings"],
output_names=["iou_scores", "pred_masks"],
dynamic_axes={
"input_points": {0: "batch_size", 1: "point_batch_size", 2: "nb_points_per_image"},
"input_labels": {0: "batch_size", 1: "point_batch_size", 2: "nb_points_per_image"},
"image_embeddings": {0: "batch_size"},
"image_positional_embeddings": {0: "batch_size"},
"iou_scores": {0: "batch_size", 1: "point_batch_size"},
"pred_masks": {0: "batch_size", 1: "point_batch_size"},
},
opset_version=17,
dynamo=False,
)
quantize_dynamic(fp32, q8, weight_type=QuantType.QUInt8)
# Check against PyTorch SamModel on a real-looking embedding.
from PIL import Image, ImageDraw
from transformers import SamProcessor
proc = SamProcessor.from_pretrained(MODEL_ID)
img = Image.new("RGB", (1024, 768), (122, 130, 140))
d = ImageDraw.Draw(img)
d.ellipse((150, 250, 450, 550), fill=(225, 30, 30))
d.rectangle((600, 150, 900, 450), fill=(30, 60, 220))
with torch.inference_mode():
emb = sam.get_image_embeddings(proc(images=img, return_tensors="pt")["pixel_values"])
pos = sam.get_image_wide_positional_embeddings()
# Coordinates already in the 1024 frame (the image's long side is 1024).
cases = {
"click (points only)": dict(points=[[300, 400]], labels=[1], box=None),
"two points": dict(points=[[300, 400], [750, 300]], labels=[1, 0], box=None),
"box + point": dict(points=[[300, 400]], labels=[1], box=[100, 200, 950, 600]),
"box + 3 points": dict(points=[[300, 400], [750, 300], [200, 300]], labels=[1, 1, 1], box=[100, 120, 950, 600]),
}
worst = 0.0
for path in (fp32, q8):
sess = ort.InferenceSession(path, providers=["CPUExecutionProvider"])
for name, c in cases.items():
p = torch.tensor([[c["points"]]], dtype=torch.float)
l = torch.tensor([[c["labels"]]], dtype=torch.int64)
with torch.inference_mode():
ref = sam(image_embeddings=emb, input_points=p, input_labels=l,
input_boxes=None if c["box"] is None else torch.tensor([[c["box"]]], dtype=torch.float),
multimask_output=True)
if c["box"] is None:
xp = torch.cat([p, torch.zeros(1, 1, 1, 2)], dim=2)
xl = torch.cat([l, -torch.ones(1, 1, 1, dtype=torch.int64)], dim=2)
else:
b = c["box"]
xp = torch.cat([p, torch.tensor([[[[b[0], b[1]], [b[2], b[3]]]]], dtype=torch.float)], dim=2)
xl = torch.cat([l, torch.tensor([[[2, 3]]], dtype=torch.int64)], dim=2)
iou, masks = sess.run(None, {
"input_points": xp.numpy(), "input_labels": xl.numpy(),
"image_embeddings": emb.numpy(), "image_positional_embeddings": pos.numpy(),
})
ref_m = ref.pred_masks.numpy() > 0
agree = float((ref_m == (masks > 0)).mean())
d_iou = float(np.abs(ref.iou_scores.numpy() - iou).max())
tag = "fp32" if path == fp32 else "q8"
print(f"{tag:5} {name:22} mask agreement {agree:.5f} iou diff {d_iou:.4f}")
if path == fp32:
worst = max(worst, 1 - agree)
if worst > 1e-4:
sys.exit(f"fp32 export disagrees with PyTorch ({worst:.5f} of mask pixels)")
print("ok", fp32, os.path.getsize(fp32), q8, os.path.getsize(q8))
if __name__ == "__main__":
main(sys.argv[1] if len(sys.argv) > 1 else "slimsam-boxes")
|