Image Segmentation
Transformers
Safetensors
sam2
instance-segmentation
panoptic-segmentation
semantic-segmentation
zero-shot
open-vocabulary
beit3
fiftyone
Instructions to use Voxel51/openworld-sam with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use Voxel51/openworld-sam with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("image-segmentation", model="Voxel51/openworld-sam")# Load model directly from transformers import AutoModel model = AutoModel.from_pretrained("Voxel51/openworld-sam", dtype="auto") - sam2
How to use Voxel51/openworld-sam with sam2:
# Use SAM2 with images import torch from sam2.sam2_image_predictor import SAM2ImagePredictor predictor = SAM2ImagePredictor.from_pretrained(Voxel51/openworld-sam) with torch.inference_mode(), torch.autocast("cuda", dtype=torch.bfloat16): predictor.set_image(<your_image>) masks, _, _ = predictor.predict(<input_prompts>)# Use SAM2 with videos import torch from sam2.sam2_video_predictor import SAM2VideoPredictor predictor = SAM2VideoPredictor.from_pretrained(Voxel51/openworld-sam) with torch.inference_mode(), torch.autocast("cuda", dtype=torch.bfloat16): state = predictor.init_state(<your_video>) # add new prompts and instantly get the output on the same frame frame_idx, object_ids, masks = predictor.add_new_points(state, <your_prompts>): # propagate the prompts to get masklets throughout the video for frame_idx, object_ids, masks in predictor.propagate_in_video(state): ... - Notebooks
- Google Colab
- Kaggle
| """OpenWorldSAM: self-contained HuggingFace model (no detectron2). | |
| Original paper: "Extending SAM2 for Universal Image Segmentation with Language Prompts" | |
| (Xiao et al., NeurIPS 2025 Spotlight). Original code: GinnyXiao/OpenWorldSAM (Apache-2.0). | |
| Architecture: | |
| evf_sam2 — EvfSam2Model (SAM2 Hiera-Large + BEiT-3 multimodal encoder) | |
| text_hidden_fcs — 3-layer projection MLP: BEiT-3 hidden_dim → query_dim | |
| positional_tokens — learnable positional embeddings [num_tokens, query_dim] | |
| cross_attention_transformer — 3-layer cross-attention stack | |
| """ | |
| import numpy as np | |
| import torch | |
| import torch.nn as nn | |
| import torch.nn.functional as F | |
| import torchvision | |
| from transformers import PreTrainedModel, AutoTokenizer | |
| # Absolute imports: the repo root must be on sys.path (handled by the FiftyOne | |
| # loader or by HF trust_remote_code loading from a local snapshot directory). | |
| from configuration_openworld_sam import OpenWorldSAMConfig # noqa: E402 | |
| # Trigger Hydra config registration before any SAM2 imports | |
| from model import evf_sam2 as _evf_module # noqa: F401, E402 | |
| from model.evf_sam2 import EvfSam2Model # noqa: E402 | |
| # --------------------------------------------------------------------------- | |
| # Cross-attention transformer (inlined from model/open_world_sam2.py) | |
| # --------------------------------------------------------------------------- | |
| class _CrossAttentionLayer(nn.Module): | |
| def __init__(self, embedding_dim, num_heads, mlp_dim, dropout=0.1): | |
| super().__init__() | |
| self.self_attn_norm = nn.LayerNorm(embedding_dim) | |
| self.self_attn = nn.MultiheadAttention(embedding_dim, num_heads, dropout=dropout, batch_first=True) | |
| self.self_attn_dropout = nn.Dropout(dropout) | |
| self.cross_attn_norm = nn.LayerNorm(embedding_dim) | |
| self.cross_attn = nn.MultiheadAttention(embedding_dim, num_heads, dropout=dropout, batch_first=True) | |
| self.cross_attn_dropout = nn.Dropout(dropout) | |
| self.mlp_norm = nn.LayerNorm(embedding_dim) | |
| self.mlp = nn.Sequential( | |
| nn.Linear(embedding_dim, mlp_dim), | |
| nn.GELU(), | |
| nn.Dropout(dropout), | |
| nn.Linear(mlp_dim, embedding_dim), | |
| nn.Dropout(dropout), | |
| ) | |
| def forward(self, vlm_features, image_embeddings): | |
| # Self-attention | |
| r = vlm_features | |
| x = self.self_attn_norm(vlm_features) | |
| x, _ = self.self_attn(x, x, x) | |
| x = r + self.self_attn_dropout(x) | |
| # Cross-attention | |
| r = x | |
| x = self.cross_attn_norm(x) | |
| x, _ = self.cross_attn(query=x, key=image_embeddings, value=image_embeddings) | |
| x = r + self.cross_attn_dropout(x) | |
| # MLP | |
| r = x | |
| x = self.mlp_norm(x) | |
| x = self.mlp(x) | |
| return r + x | |
| class _CrossAttentionTransformer(nn.Module): | |
| def __init__(self, embedding_dim, num_heads, mlp_dim, num_layers=3, dropout=0.1): | |
| super().__init__() | |
| self.layers = nn.ModuleList([ | |
| _CrossAttentionLayer(embedding_dim, num_heads, mlp_dim, dropout) | |
| for _ in range(num_layers) | |
| ]) | |
| def forward(self, vlm_features, image_embeddings): | |
| x = vlm_features | |
| for layer in self.layers: | |
| x = layer(x, image_embeddings) | |
| return x | |
| # --------------------------------------------------------------------------- | |
| # Pure-torch helpers (replacing detectron2 structures) | |
| # --------------------------------------------------------------------------- | |
| def _masks_to_boxes(masks): | |
| """bool tensor [N, H, W] → float tensor [N, 4] xyxy bounding boxes.""" | |
| n = masks.shape[0] | |
| boxes = torch.zeros((n, 4), dtype=torch.float32, device=masks.device) | |
| for i in range(n): | |
| m = masks[i] | |
| rows = m.any(dim=1).nonzero(as_tuple=False) | |
| cols = m.any(dim=0).nonzero(as_tuple=False) | |
| if rows.numel() > 0 and cols.numel() > 0: | |
| boxes[i] = torch.tensor( | |
| [cols[0].item(), rows[0].item(), cols[-1].item() + 1, rows[-1].item() + 1], | |
| dtype=torch.float32, | |
| device=masks.device, | |
| ) | |
| return boxes | |
| # --------------------------------------------------------------------------- | |
| # Main PreTrainedModel | |
| # --------------------------------------------------------------------------- | |
| class OpenWorldSAMModel(PreTrainedModel): | |
| """OpenWorldSAM zero-shot segmentation model (HuggingFace trust_remote_code). | |
| Usage:: | |
| from transformers import AutoModel | |
| model = AutoModel.from_pretrained( | |
| "neerajaabhyankar/openworld-sam", trust_remote_code=True | |
| ) | |
| # batched_inputs: list of dicts with keys: | |
| # "image" — float32 SAM-normalised tensor [3, 1024, 1024] | |
| # "evf_image" — float32 BEiT-3 tensor [3, 224, 224] | |
| # "height", "width" — original image dimensions (int) | |
| # "prompt" — list[str] of text prompts | |
| # "unique_categories" — list[int] of category ids (one per prompt) | |
| outputs = model(batched_inputs) | |
| # outputs: list of dicts, one per image; key "instances" holds masks/scores/class_ids | |
| """ | |
| config_class = OpenWorldSAMConfig | |
| def __init__(self, config: OpenWorldSAMConfig): | |
| super().__init__(config) | |
| # EVF-SAM2 backbone (SAM2 Hiera-L + BEiT-3) | |
| from model.configuration_evf import EvfConfig | |
| evf_cfg = EvfConfig( | |
| hidden_size=1024, | |
| sam_scale=config.sam_scale, | |
| mm_extractor_scale=config.mm_extractor_scale, | |
| ) | |
| self.evf_sam2 = EvfSam2Model(evf_cfg) | |
| # Projection MLP: BEiT-3 hidden (1024) → query_dim (256) | |
| in_dim = 1024 # BEiT-3 large hidden size | |
| qd = config.query_dim | |
| self.text_hidden_fcs = nn.ModuleList([ | |
| nn.Sequential(nn.Linear(in_dim, in_dim), nn.ReLU(), nn.Linear(in_dim, qd)) | |
| ]) | |
| # Learnable positional tokens [num_tokens, query_dim] | |
| self.positional_tokens = nn.Parameter(torch.randn(config.num_tokens, qd)) | |
| # Cross-attention transformer | |
| self.cross_attention_transformer = _CrossAttentionTransformer( | |
| embedding_dim=qd, | |
| num_heads=8, | |
| mlp_dim=qd * 4, | |
| num_layers=config.cross_attention_layers, | |
| ) | |
| # SAM2 feature size schedule (matches Hiera-L) | |
| self._bb_feat_sizes = [(256, 256), (128, 128), (64, 64)] | |
| # Preprocessing buffers | |
| self.register_buffer( | |
| "pixel_mean", | |
| torch.tensor(config.pixel_mean).view(-1, 1, 1), | |
| persistent=False, | |
| ) | |
| self.register_buffer( | |
| "pixel_std", | |
| torch.tensor(config.pixel_std).view(-1, 1, 1), | |
| persistent=False, | |
| ) | |
| # Tokenizer loaded lazily on first forward | |
| self._tokenizer = None | |
| # Required by transformers>=5's from_pretrained (sets | |
| # self.all_tied_weights_keys and other bookkeeping consumed by | |
| # _finalize_model_loading); harmless no-op pre-checkpoint-load init. | |
| self.post_init() | |
| # ------------------------------------------------------------------ | |
| # Tokenizer | |
| # ------------------------------------------------------------------ | |
| def tokenizer(self): | |
| if self._tokenizer is None: | |
| self._tokenizer = AutoTokenizer.from_pretrained( | |
| self.config.tokenizer_name_or_path, | |
| padding_side="right", | |
| use_fast=False, | |
| ) | |
| return self._tokenizer | |
| def _tokenize_prompts(self, prompts): | |
| tok = self.tokenizer | |
| ids = [tok(p, return_tensors="pt").input_ids[0] for p in prompts] | |
| ids = torch.nn.utils.rnn.pad_sequence(ids, batch_first=True, padding_value=tok.pad_token_id) | |
| masks = ids.ne(tok.pad_token_id) | |
| trunc = tok.model_max_length | |
| return ids[:, :trunc].to(self.device), masks[:, :trunc].to(self.device) | |
| # ------------------------------------------------------------------ | |
| # Forward | |
| # ------------------------------------------------------------------ | |
| def forward(self, batched_inputs): | |
| """ | |
| Args: | |
| batched_inputs: list of dicts, one per image: | |
| "image" float32 [3, 1024, 1024], SAM normalised | |
| "evf_image" float32 [3, 224, 224], BEiT-3 normalised | |
| "height", "width" int, original image size | |
| "prompt" list[str] | |
| "unique_categories" list[int] | |
| Returns: | |
| list of dicts, one per image. Key "instances" is a dict: | |
| "masks" bool tensor [N, H, W] | |
| "scores" float tensor [N] | |
| "class_ids" long tensor [N] | |
| """ | |
| dtype = torch.float32 | |
| images = torch.stack([x["image"].to(dtype=dtype, device=self.device) for x in batched_inputs]) | |
| images_evf = torch.stack([x["evf_image"].to(dtype=dtype, device=self.device) for x in batched_inputs]) | |
| original_size_list = [(x["height"], x["width"]) for x in batched_inputs] | |
| # Build flattened prompt list with per-image offsets | |
| offset = [0] | |
| all_prompts = [] | |
| for x in batched_inputs: | |
| all_prompts.extend(x["prompt"]) | |
| offset.append(offset[-1] + len(x["prompt"])) | |
| input_ids, attention_masks = self._tokenize_prompts(all_prompts) | |
| batch_size = len(batched_inputs) | |
| # SAM2 visual encoder | |
| with torch.no_grad(): | |
| backbone_out = self.evf_sam2.visual_model.forward_image(images) | |
| _, image_embeddings, _, _ = self.evf_sam2.visual_model._prepare_backbone_features(backbone_out) | |
| image_embeddings = [e.to(dtype) for e in image_embeddings] | |
| if self.evf_sam2.visual_model.directly_add_no_mem_embed: | |
| image_embeddings[-1] = image_embeddings[-1] + self.evf_sam2.visual_model.no_mem_embed | |
| # Expand images_evf per prompt count if using visual tokens | |
| if self.config.use_visual_tokens: | |
| imgs_list = [] | |
| for i in range(batch_size): | |
| n = offset[i + 1] - offset[i] | |
| imgs_list.append(images_evf[i].unsqueeze(0).expand(n, -1, -1, -1).contiguous()) | |
| images_evf_expanded = torch.cat(imgs_list, dim=0) | |
| else: | |
| images_evf_expanded = None | |
| # BEiT-3 multimodal encoding | |
| with torch.no_grad(): | |
| if images_evf_expanded is not None: | |
| out = self.evf_sam2.mm_extractor.beit3( | |
| visual_tokens=images_evf_expanded, | |
| textual_tokens=input_ids, | |
| text_padding_position=~attention_masks, | |
| ) | |
| else: | |
| out = self.evf_sam2.mm_extractor.beit3( | |
| visual_tokens=None, | |
| textual_tokens=input_ids, | |
| text_padding_position=~attention_masks, | |
| ) | |
| feat = out["encoder_out"][:, :1, ...] # [total_prompts, 1, hidden] | |
| feat = self.text_hidden_fcs[0](feat) # [total_prompts, 1, query_dim] | |
| # Split back per image | |
| feat = torch.split(feat, [offset[i + 1] - offset[i] for i in range(batch_size)]) | |
| # Multi-scale image feature tensor | |
| feats = [ | |
| e.permute(1, 2, 0).view(batch_size, -1, *sz) | |
| for e, sz in zip(image_embeddings[::-1], self._bb_feat_sizes[::-1]) | |
| ][::-1] | |
| _features = {"image_embed": feats[-1], "high_res_feats": feats[:-1]} | |
| processed_results = [] | |
| for img_idx in range(batch_size): | |
| img_feat = feat[img_idx] # [num_prompts, 1, query_dim] | |
| # Build batch_feat_with_tokens | |
| tokens_list = [] | |
| for pf in img_feat: | |
| repeated = pf.expand(self.config.num_tokens, -1, -1) # [num_tokens, 1, query_dim] | |
| tokens_list.append(repeated + self.positional_tokens.unsqueeze(1)) | |
| batch_feat_with_tokens = torch.cat(tokens_list, dim=0) # [total_tokens, 1, query_dim] | |
| # Cross-attention with skip connection | |
| if self.config.use_cross_attention: | |
| img_embed = _features["image_embed"][img_idx].flatten(1).transpose(0, 1).unsqueeze(0) | |
| # batch_feat_with_tokens: [total_tokens, 1, qd] → squeeze middle → [1, total_tokens, qd] | |
| bft = batch_feat_with_tokens.squeeze(1).unsqueeze(0) | |
| enhanced = self.cross_attention_transformer(bft, img_embed) # [1, total_tokens, qd] | |
| # Reshape back to [total_tokens, 1, qd] and add skip connection | |
| enhanced = enhanced.squeeze(0).unsqueeze(1) | |
| batch_feat_with_tokens = batch_feat_with_tokens + enhanced | |
| # SAM2 prompt encoder + mask decoder | |
| sparse_embeddings, dense_embeddings = self.evf_sam2.visual_model.sam_prompt_encoder( | |
| points=None, boxes=None, masks=None, text_embeds=batch_feat_with_tokens, | |
| ) | |
| sparse_embeddings = sparse_embeddings.to(batch_feat_with_tokens.dtype) | |
| high_res_features = [ | |
| f[img_idx].unsqueeze(0) for f in _features["high_res_feats"] | |
| ] | |
| # The decoder's repeat_image path duplicates image_embeddings and | |
| # image_pe once per candidate query, so scoring all num_classes * | |
| # num_tokens candidates in one call can reach double-digit GiB | |
| # for large vocabularies. `chunk_size` bounds peak memory | |
| # rather than total candidate count. | |
| image_embed_img = _features["image_embed"][img_idx].unsqueeze(0) | |
| image_pe = self.evf_sam2.visual_model.sam_prompt_encoder.get_dense_pe() | |
| chunk_size = self.config.mask_decoder_chunk_size | |
| num_total_tokens = sparse_embeddings.shape[0] | |
| low_res_masks_chunks = [] | |
| iou_pred_chunks = [] | |
| with torch.no_grad(): | |
| for start in range(0, num_total_tokens, chunk_size): | |
| end = min(start + chunk_size, num_total_tokens) | |
| chunk_low_res_masks, chunk_iou_pred, _, _ = self.evf_sam2.visual_model.sam_mask_decoder( | |
| image_embeddings=image_embed_img, | |
| image_pe=image_pe, | |
| sparse_prompt_embeddings=sparse_embeddings[start:end], | |
| dense_prompt_embeddings=dense_embeddings[start:end], | |
| multimask_output=False, | |
| repeat_image=True, | |
| high_res_features=high_res_features, | |
| ) | |
| low_res_masks_chunks.append(chunk_low_res_masks) | |
| iou_pred_chunks.append(chunk_iou_pred) | |
| low_res_masks = torch.cat(low_res_masks_chunks, dim=0) | |
| iou_pred = torch.cat(iou_pred_chunks, dim=0) | |
| pred_masks = low_res_masks.squeeze(1) # [total_tokens, H_low, W_low] | |
| pred_logits = iou_pred.squeeze(1) # [total_tokens] | |
| # Assign class labels: each prompt gets num_tokens predictions | |
| unique_categories = batched_inputs[img_idx]["unique_categories"] | |
| num_total = pred_masks.shape[0] | |
| class_indices = torch.div( | |
| torch.arange(num_total, device=self.device), | |
| self.config.num_tokens, rounding_mode="floor" | |
| ) | |
| class_labels = torch.tensor( | |
| [unique_categories[int(i)] for i in class_indices], | |
| dtype=torch.long, device=self.device, | |
| ) | |
| # Filter on low-res masks first; only the survivors get upsampled | |
| # to the original image size (upsampling the full query set before | |
| # filtering allocates one [num_queries, H, W] float32 tensor that | |
| # can reach double-digit GiB for large vocabularies). | |
| instances = self._instance_inference( | |
| pred_masks, pred_logits, class_labels, original_size_list[img_idx] | |
| ) | |
| processed_results.append({"instances": instances}) | |
| return processed_results | |
| def _postprocess_masks(self, masks, orig_hw): | |
| return F.interpolate( | |
| masks.float().unsqueeze(0), orig_hw, mode="bilinear", align_corners=False | |
| ).squeeze(0) | |
| def _instance_inference(self, pred_masks, iou_scores, class_labels, orig_hw): | |
| """Returns dict with keys: masks (bool), scores (float), class_ids (long).""" | |
| pred_masks = pred_masks.squeeze(1) if pred_masks.ndim == 4 else pred_masks | |
| # Top-K filter | |
| if self.config.top_k_on: | |
| k = min(self.config.detections_per_image, pred_masks.shape[0]) | |
| idx = torch.argsort(iou_scores, descending=True)[:k] | |
| pred_masks, iou_scores, class_labels = pred_masks[idx], iou_scores[idx], class_labels[idx] | |
| # IoU threshold filter | |
| keep = iou_scores >= self.config.iou_thresh | |
| pred_masks, iou_scores, class_labels = pred_masks[keep], iou_scores[keep], class_labels[keep] | |
| if pred_masks.shape[0] == 0: | |
| empty = torch.empty(0, device=self.device) | |
| return { | |
| "masks": torch.empty((0, *orig_hw), dtype=torch.bool, device=self.device), | |
| "scores": empty, | |
| "class_ids": empty.long(), | |
| } | |
| # NMS on low-res masks — box IoU is scale-invariant, so this doesn't | |
| # need the full-res masks either. | |
| low_res_binary_masks = pred_masks > 0 | |
| if self.config.nms_on: | |
| boxes = _masks_to_boxes(low_res_binary_masks) | |
| nms_keep = torchvision.ops.nms(boxes, iou_scores, self.config.nms_thresh) | |
| pred_masks = pred_masks[nms_keep] | |
| iou_scores = iou_scores[nms_keep] | |
| class_labels = class_labels[nms_keep] | |
| # Upsample only the surviving masks to the original image size | |
| pred_masks = self._postprocess_masks(pred_masks, orig_hw) | |
| binary_masks = pred_masks > 0 | |
| return { | |
| "masks": binary_masks, | |
| "scores": iou_scores, | |
| "class_ids": class_labels, | |
| } | |
| # ------------------------------------------------------------------ | |
| # Convenience preprocessing (mirrors demo/inference_utils.py) | |
| # ------------------------------------------------------------------ | |
| def preprocess_image(self, image): | |
| """Normalise a uint8 HWC numpy array (RGB) → float32 [3, 1024, 1024] tensor on model device.""" | |
| if not isinstance(image, np.ndarray): | |
| image = np.array(image) | |
| tensor = torch.as_tensor(np.ascontiguousarray(image.transpose(2, 0, 1))).float() | |
| tensor = F.interpolate( | |
| tensor.unsqueeze(0), (1024, 1024), mode="bilinear", align_corners=False | |
| ).squeeze(0) | |
| return (tensor - self.pixel_mean.cpu()) / self.pixel_std.cpu() | |
| def preprocess_image_beit3(self, image): | |
| """Normalise a uint8 HWC numpy array (RGB) → float32 [3, 224, 224] tensor.""" | |
| from torchvision import transforms | |
| from PIL import Image as PILImage | |
| if isinstance(image, np.ndarray): | |
| pil = PILImage.fromarray(image.astype(np.uint8)) | |
| else: | |
| pil = image | |
| tf = transforms.Compose([ | |
| transforms.ToTensor(), | |
| transforms.Resize((224, 224), interpolation=3, antialias=None), | |
| transforms.Normalize(mean=(0.5, 0.5, 0.5), std=(0.5, 0.5, 0.5)), | |
| ]) | |
| return tf(pil) | |