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", device_map="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
Add CLIP-based semantic re-ranking for instance segmentation
#3
by neerajaabhyankar - opened
- configuration_openworld_sam.py +19 -0
- modeling_openworld_sam.py +144 -6
configuration_openworld_sam.py
CHANGED
|
@@ -53,6 +53,21 @@ class OpenWorldSAMConfig(PretrainedConfig):
|
|
| 53 |
iou_thresh=0.7,
|
| 54 |
top_k_on=False,
|
| 55 |
detections_per_image=100,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
# Class vocabulary
|
| 57 |
stuff_classes=None,
|
| 58 |
**kwargs,
|
|
@@ -74,4 +89,8 @@ class OpenWorldSAMConfig(PretrainedConfig):
|
|
| 74 |
self.iou_thresh = iou_thresh
|
| 75 |
self.top_k_on = top_k_on
|
| 76 |
self.detections_per_image = detections_per_image
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
self.stuff_classes = stuff_classes if stuff_classes is not None else ADE20K_150_CLASSES
|
|
|
|
| 53 |
iou_thresh=0.7,
|
| 54 |
top_k_on=False,
|
| 55 |
detections_per_image=100,
|
| 56 |
+
# Mask decoder batching: the decoder's repeat_image path duplicates
|
| 57 |
+
# the image embeddings/positional encoding once per candidate query,
|
| 58 |
+
# so running all num_classes * num_tokens candidates through it in
|
| 59 |
+
# one call can reach double-digit GiB for large vocabularies. This
|
| 60 |
+
# bounds peak memory by running the decoder in chunks instead.
|
| 61 |
+
mask_decoder_chunk_size=500,
|
| 62 |
+
# CLIP-based re-ranking: the SAM mask-quality (IoU) score measures
|
| 63 |
+
# only whether a mask is well-formed, not whether it matches the
|
| 64 |
+
# queried class, so class-agnostic mask proposals conditioned on
|
| 65 |
+
# different prompts routinely tie on score for the same object. When
|
| 66 |
+
# enabled, surviving candidates (post iou_thresh) are re-scored and
|
| 67 |
+
# re-labeled using actual CLIP image/text similarity instead.
|
| 68 |
+
use_clip_rerank=True,
|
| 69 |
+
clip_model_name_or_path="openai/clip-vit-base-patch32",
|
| 70 |
+
clip_crop_padding_frac=0.1,
|
| 71 |
# Class vocabulary
|
| 72 |
stuff_classes=None,
|
| 73 |
**kwargs,
|
|
|
|
| 89 |
self.iou_thresh = iou_thresh
|
| 90 |
self.top_k_on = top_k_on
|
| 91 |
self.detections_per_image = detections_per_image
|
| 92 |
+
self.mask_decoder_chunk_size = mask_decoder_chunk_size
|
| 93 |
+
self.use_clip_rerank = use_clip_rerank
|
| 94 |
+
self.clip_model_name_or_path = clip_model_name_or_path
|
| 95 |
+
self.clip_crop_padding_frac = clip_crop_padding_frac
|
| 96 |
self.stuff_classes = stuff_classes if stuff_classes is not None else ADE20K_150_CLASSES
|
modeling_openworld_sam.py
CHANGED
|
@@ -180,6 +180,10 @@ class OpenWorldSAMModel(PreTrainedModel):
|
|
| 180 |
# Tokenizer loaded lazily on first forward
|
| 181 |
self._tokenizer = None
|
| 182 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
# Required by transformers>=5's from_pretrained (sets
|
| 184 |
# self.all_tied_weights_keys and other bookkeeping consumed by
|
| 185 |
# _finalize_model_loading); harmless no-op pre-checkpoint-load init.
|
|
@@ -199,6 +203,31 @@ class OpenWorldSAMModel(PreTrainedModel):
|
|
| 199 |
)
|
| 200 |
return self._tokenizer
|
| 201 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 202 |
def _tokenize_prompts(self, prompts):
|
| 203 |
tok = self.tokenizer
|
| 204 |
ids = [tok(p, return_tensors="pt").input_ids[0] for p in prompts]
|
|
@@ -388,11 +417,11 @@ class OpenWorldSAMModel(PreTrainedModel):
|
|
| 388 |
iou_pred_all = torch.cat(iou_pred_chunks, dim=0)
|
| 389 |
|
| 390 |
pred_masks_all = low_res_masks_all.squeeze(1) # [total_tokens_all, H_low, W_low]
|
| 391 |
-
pred_logits_all = iou_pred_all.squeeze(1)
|
| 392 |
-
|
| 393 |
pred_masks_per_image = torch.split(pred_masks_all, token_counts)
|
| 394 |
pred_logits_per_image = torch.split(pred_logits_all, token_counts)
|
| 395 |
-
|
| 396 |
processed_results = []
|
| 397 |
for img_idx in range(batch_size):
|
| 398 |
pred_masks = pred_masks_per_image[img_idx]
|
|
@@ -410,14 +439,31 @@ class OpenWorldSAMModel(PreTrainedModel):
|
|
| 410 |
dtype=torch.long, device=self.device,
|
| 411 |
)
|
| 412 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 413 |
# Filter on low-res masks first; only the survivors get upsampled
|
| 414 |
# to the original image size (upsampling the full query set before
|
| 415 |
# filtering allocates one [num_queries, H, W] float32 tensor that
|
| 416 |
# can reach double-digit GiB for large vocabularies).
|
| 417 |
instances = self._instance_inference(
|
| 418 |
-
pred_masks, pred_logits, class_labels, original_size_list[img_idx]
|
|
|
|
| 419 |
)
|
| 420 |
-
|
| 421 |
processed_results.append({"instances": instances})
|
| 422 |
|
| 423 |
return processed_results
|
|
@@ -427,7 +473,93 @@ class OpenWorldSAMModel(PreTrainedModel):
|
|
| 427 |
masks.float().unsqueeze(0), orig_hw, mode="bilinear", align_corners=False
|
| 428 |
).squeeze(0)
|
| 429 |
|
| 430 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 431 |
"""Returns dict with keys: masks (bool), scores (float), class_ids (long)."""
|
| 432 |
pred_masks = pred_masks.squeeze(1) if pred_masks.ndim == 4 else pred_masks
|
| 433 |
|
|
@@ -449,6 +581,12 @@ class OpenWorldSAMModel(PreTrainedModel):
|
|
| 449 |
"class_ids": empty.long(),
|
| 450 |
}
|
| 451 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 452 |
|
| 453 |
# NMS on low-res masks — box IoU is scale-invariant, so this doesn't
|
| 454 |
# need the full-res masks either.
|
|
|
|
| 180 |
# Tokenizer loaded lazily on first forward
|
| 181 |
self._tokenizer = None
|
| 182 |
|
| 183 |
+
# CLIP model/processor (semantic re-ranking) loaded lazily on first use
|
| 184 |
+
self._clip_model = None
|
| 185 |
+
self._clip_processor = None
|
| 186 |
+
|
| 187 |
# Required by transformers>=5's from_pretrained (sets
|
| 188 |
# self.all_tied_weights_keys and other bookkeeping consumed by
|
| 189 |
# _finalize_model_loading); harmless no-op pre-checkpoint-load init.
|
|
|
|
| 203 |
)
|
| 204 |
return self._tokenizer
|
| 205 |
|
| 206 |
+
# ------------------------------------------------------------------
|
| 207 |
+
# CLIP (semantic re-ranking)
|
| 208 |
+
# ------------------------------------------------------------------
|
| 209 |
+
|
| 210 |
+
@property
|
| 211 |
+
def clip_model(self):
|
| 212 |
+
if self._clip_model is None:
|
| 213 |
+
from transformers import CLIPModel
|
| 214 |
+
|
| 215 |
+
self._clip_model = CLIPModel.from_pretrained(
|
| 216 |
+
self.config.clip_model_name_or_path
|
| 217 |
+
).to(self.device)
|
| 218 |
+
self._clip_model.eval()
|
| 219 |
+
return self._clip_model
|
| 220 |
+
|
| 221 |
+
@property
|
| 222 |
+
def clip_processor(self):
|
| 223 |
+
if self._clip_processor is None:
|
| 224 |
+
from transformers import CLIPProcessor
|
| 225 |
+
|
| 226 |
+
self._clip_processor = CLIPProcessor.from_pretrained(
|
| 227 |
+
self.config.clip_model_name_or_path
|
| 228 |
+
)
|
| 229 |
+
return self._clip_processor
|
| 230 |
+
|
| 231 |
def _tokenize_prompts(self, prompts):
|
| 232 |
tok = self.tokenizer
|
| 233 |
ids = [tok(p, return_tensors="pt").input_ids[0] for p in prompts]
|
|
|
|
| 417 |
iou_pred_all = torch.cat(iou_pred_chunks, dim=0)
|
| 418 |
|
| 419 |
pred_masks_all = low_res_masks_all.squeeze(1) # [total_tokens_all, H_low, W_low]
|
| 420 |
+
pred_logits_all = iou_pred_all.squeeze(1) # [total_tokens_all]
|
| 421 |
+
|
| 422 |
pred_masks_per_image = torch.split(pred_masks_all, token_counts)
|
| 423 |
pred_logits_per_image = torch.split(pred_logits_all, token_counts)
|
| 424 |
+
|
| 425 |
processed_results = []
|
| 426 |
for img_idx in range(batch_size):
|
| 427 |
pred_masks = pred_masks_per_image[img_idx]
|
|
|
|
| 439 |
dtype=torch.long, device=self.device,
|
| 440 |
)
|
| 441 |
|
| 442 |
+
square_rgb = None
|
| 443 |
+
id_to_text = None
|
| 444 |
+
if self.config.use_clip_rerank:
|
| 445 |
+
# Reads from config directly rather than the pixel_mean/std
|
| 446 |
+
# buffers: those are registered persistent=False (excluded
|
| 447 |
+
# from the checkpoint on purpose), but transformers'
|
| 448 |
+
# from_pretrained fast-init path zeroes any buffer it
|
| 449 |
+
# doesn't find a matching checkpoint key for, config values
|
| 450 |
+
# aren't affected by that.
|
| 451 |
+
mean = torch.tensor(self.config.pixel_mean, device=images.device).view(-1, 1, 1)
|
| 452 |
+
std = torch.tensor(self.config.pixel_std, device=images.device).view(-1, 1, 1)
|
| 453 |
+
denorm = images[img_idx].float() * std + mean
|
| 454 |
+
square_rgb = denorm.clamp(0, 255).round().byte().permute(1, 2, 0).cpu().numpy()
|
| 455 |
+
id_to_text = dict(zip(
|
| 456 |
+
unique_categories, batched_inputs[img_idx]["prompt"]
|
| 457 |
+
))
|
| 458 |
+
|
| 459 |
# Filter on low-res masks first; only the survivors get upsampled
|
| 460 |
# to the original image size (upsampling the full query set before
|
| 461 |
# filtering allocates one [num_queries, H, W] float32 tensor that
|
| 462 |
# can reach double-digit GiB for large vocabularies).
|
| 463 |
instances = self._instance_inference(
|
| 464 |
+
pred_masks, pred_logits, class_labels, original_size_list[img_idx],
|
| 465 |
+
square_rgb=square_rgb, id_to_text=id_to_text,
|
| 466 |
)
|
|
|
|
| 467 |
processed_results.append({"instances": instances})
|
| 468 |
|
| 469 |
return processed_results
|
|
|
|
| 473 |
masks.float().unsqueeze(0), orig_hw, mode="bilinear", align_corners=False
|
| 474 |
).squeeze(0)
|
| 475 |
|
| 476 |
+
def _clip_rerank(self, pred_masks, class_labels, square_rgb, id_to_text):
|
| 477 |
+
"""Re-scores/re-labels candidates using real CLIP image/text similarity.
|
| 478 |
+
|
| 479 |
+
The SAM mask-quality (IoU) score measures whether a mask is well
|
| 480 |
+
formed, not whether it matches the queried class: each prompt is run
|
| 481 |
+
through cross-attention independently, so several unrelated prompts
|
| 482 |
+
routinely produce near-identical masks over the same salient object,
|
| 483 |
+
each with a similarly high IoU score. This crops each surviving
|
| 484 |
+
candidate's mask region out of the original image and asks a real
|
| 485 |
+
(frozen, off-the-shelf) CLIP model which of the candidate class names
|
| 486 |
+
actually matches that crop, replacing both the class label and the
|
| 487 |
+
score with CLIP's pick/confidence.
|
| 488 |
+
|
| 489 |
+
Args:
|
| 490 |
+
pred_masks: bool/float tensor [N, h, w], low-res mask logits/mask
|
| 491 |
+
class_labels: long tensor [N], the prompt each mask was proposed under
|
| 492 |
+
square_rgb: uint8 numpy array [H, W, 3], the (square) SAM input image
|
| 493 |
+
id_to_text: dict mapping category id -> prompt string
|
| 494 |
+
|
| 495 |
+
Returns:
|
| 496 |
+
(class_labels, scores) tensors, both length N
|
| 497 |
+
"""
|
| 498 |
+
n = pred_masks.shape[0]
|
| 499 |
+
if n == 0:
|
| 500 |
+
return class_labels, torch.empty(0, device=self.device)
|
| 501 |
+
|
| 502 |
+
from PIL import Image
|
| 503 |
+
|
| 504 |
+
H, W = square_rgb.shape[:2]
|
| 505 |
+
masks_up = F.interpolate(
|
| 506 |
+
(pred_masks > 0).float().unsqueeze(1), (H, W), mode="nearest"
|
| 507 |
+
).squeeze(1).bool()
|
| 508 |
+
|
| 509 |
+
padding_frac = self.config.clip_crop_padding_frac
|
| 510 |
+
crops = []
|
| 511 |
+
for i in range(n):
|
| 512 |
+
ys, xs = torch.where(masks_up[i])
|
| 513 |
+
if ys.numel() == 0:
|
| 514 |
+
crops.append(None)
|
| 515 |
+
continue
|
| 516 |
+
y1, y2 = int(ys.min()), int(ys.max())
|
| 517 |
+
x1, x2 = int(xs.min()), int(xs.max())
|
| 518 |
+
ph = int((y2 - y1 + 1) * padding_frac)
|
| 519 |
+
pw = int((x2 - x1 + 1) * padding_frac)
|
| 520 |
+
y1, y2 = max(0, y1 - ph), min(H, y2 + 1 + ph)
|
| 521 |
+
x1, x2 = max(0, x1 - pw), min(W, x2 + 1 + pw)
|
| 522 |
+
crops.append(Image.fromarray(square_rgb[y1:y2, x1:x2]))
|
| 523 |
+
|
| 524 |
+
unique_ids = sorted(set(id_to_text.keys()))
|
| 525 |
+
texts = [id_to_text[c] for c in unique_ids]
|
| 526 |
+
|
| 527 |
+
valid_idx = [i for i, c in enumerate(crops) if c is not None]
|
| 528 |
+
new_class_labels = class_labels.clone()
|
| 529 |
+
new_scores = torch.zeros(n, device=self.device)
|
| 530 |
+
if not valid_idx:
|
| 531 |
+
return new_class_labels, new_scores
|
| 532 |
+
|
| 533 |
+
with torch.no_grad():
|
| 534 |
+
text_inputs = self.clip_processor(
|
| 535 |
+
text=texts, return_tensors="pt", padding=True
|
| 536 |
+
).to(self.device)
|
| 537 |
+
# get_text_features/get_image_features return a
|
| 538 |
+
# BaseModelOutputWithPooling whose `.pooler_output` has been
|
| 539 |
+
# overwritten with the projected embedding, not a raw tensor.
|
| 540 |
+
text_feats = self.clip_model.get_text_features(**text_inputs).pooler_output
|
| 541 |
+
text_feats = text_feats / text_feats.norm(dim=-1, keepdim=True)
|
| 542 |
+
|
| 543 |
+
img_inputs = self.clip_processor(
|
| 544 |
+
images=[crops[i] for i in valid_idx], return_tensors="pt"
|
| 545 |
+
).to(self.device)
|
| 546 |
+
img_feats = self.clip_model.get_image_features(**img_inputs).pooler_output
|
| 547 |
+
img_feats = img_feats / img_feats.norm(dim=-1, keepdim=True)
|
| 548 |
+
|
| 549 |
+
logit_scale = self.clip_model.logit_scale.exp()
|
| 550 |
+
probs = ((img_feats @ text_feats.T) * logit_scale).softmax(dim=-1)
|
| 551 |
+
|
| 552 |
+
for row, i in enumerate(valid_idx):
|
| 553 |
+
best = int(probs[row].argmax())
|
| 554 |
+
new_class_labels[i] = unique_ids[best]
|
| 555 |
+
new_scores[i] = probs[row, best]
|
| 556 |
+
|
| 557 |
+
return new_class_labels, new_scores
|
| 558 |
+
|
| 559 |
+
def _instance_inference(
|
| 560 |
+
self, pred_masks, iou_scores, class_labels, orig_hw,
|
| 561 |
+
square_rgb=None, id_to_text=None,
|
| 562 |
+
):
|
| 563 |
"""Returns dict with keys: masks (bool), scores (float), class_ids (long)."""
|
| 564 |
pred_masks = pred_masks.squeeze(1) if pred_masks.ndim == 4 else pred_masks
|
| 565 |
|
|
|
|
| 581 |
"class_ids": empty.long(),
|
| 582 |
}
|
| 583 |
|
| 584 |
+
# Re-score/re-label using real CLIP image/text similarity instead of
|
| 585 |
+
# the SAM mask-quality score, which doesn't reflect class match.
|
| 586 |
+
if self.config.use_clip_rerank and square_rgb is not None and id_to_text is not None:
|
| 587 |
+
class_labels, iou_scores = self._clip_rerank(
|
| 588 |
+
pred_masks, class_labels, square_rgb, id_to_text
|
| 589 |
+
)
|
| 590 |
|
| 591 |
# NMS on low-res masks — box IoU is scale-invariant, so this doesn't
|
| 592 |
# need the full-res masks either.
|