neerajaabhyankar commited on
Commit
ce9d6aa
·
verified ·
1 Parent(s): 8c0f263

Upload folder using huggingface_hub

Browse files
README.md ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: mit
3
+ tags:
4
+ - panoptic-segmentation
5
+ - open-vocabulary
6
+ - zero-shot
7
+ - clip
8
+ - convnext
9
+ - mask2former
10
+ - fiftyone
11
+ library_name: transformers
12
+ pipeline_tag: image-segmentation
13
+ ---
14
+
15
+ # FC-CLIP — Open-Vocabulary Panoptic Segmentation
16
+
17
+ FC-CLIP is an open-vocabulary panoptic segmentation model that pairs a frozen
18
+ **ConvNeXt-Large CLIP** backbone with a lightweight **Mask2Former** decoder.
19
+ It achieves strong zero-shot performance without requiring separate specialist
20
+ models for things vs. stuff.
21
+
22
+ This repository hosts the COCO Panoptic checkpoint uploaded to HuggingFace Hub
23
+ by Claude, for testing use with the **FiftyOne Model Zoo**.
24
+
25
+ ## Attribution
26
+
27
+ > **Paper:** "A Simple Framework for Open-Vocabulary Segmentation and Detection"
28
+ > Jiarui Xu, Shalini De Mello, Sifei Liu, Wonmin Byeon, Thomas Breuel, Jan Kautz, Xiaolong Wang.
29
+ > CVPR 2023 · [arxiv 2311.15539](https://arxiv.org/abs/2311.15539)
30
+
31
+ > **Original code:** [bytedance/fc-clip](https://github.com/bytedance/fc-clip) — MIT License
32
+
33
+ ## Usage
34
+
35
+ ### Standalone (trust_remote_code)
36
+
37
+ ```python
38
+ import torch
39
+ from transformers import AutoModel
40
+
41
+ model = AutoModel.from_pretrained("neerajaabhyankar/fc-clip", trust_remote_code=True)
42
+ model.eval()
43
+
44
+ # Preprocess: RGB uint8 numpy/PIL → normalised tensor
45
+ pixel_values = model.preprocess_image(your_pil_image) # [1, 3, H, W]
46
+
47
+ with torch.no_grad():
48
+ results = model(pixel_values)
49
+
50
+ panoptic_seg, segments_info = results[0]
51
+ # panoptic_seg: int32 tensor [H, W] — pixel → segment id
52
+ # segments_info: list[{"id", "category_id", "isthing"}]
53
+ ```
54
+
55
+ ### Open-vocabulary (custom classes)
56
+
57
+ ```python
58
+ results = model(pixel_values, class_names=["cat", "dog", "sky", "grass"])
59
+ ```
60
+
61
+ ## Architecture
62
+
63
+ | Component | Detail |
64
+ |-----------|--------|
65
+ | Backbone | OpenCLIP ConvNeXt-Large (`convnext_large_d_320`, `laion2b_s29b_b131k_ft_soup`), **frozen** |
66
+ | Pixel decoder | 6-layer Multi-Scale Deformable Attention encoder + 1-level FPN |
67
+ | Transformer decoder | 5-layer Mask2Former cross-attention decoder, 250 queries |
68
+ | Text classification | VILD 14-template ensemble + geometric in-vocab/out-vocab blending |
69
+ | Classes | 133 COCO panoptic (80 things + 53 stuff) |
70
+
71
+ ## Requirements
72
+
73
+ ```
74
+ torch torchvision transformers open_clip_torch safetensors
75
+ ```
76
+
77
+ No `detectron2` required — the model is self-contained.
78
+
79
+ ## License
80
+
81
+ MIT (same as original bytedance/fc-clip)
config.json ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_type": "fc_clip",
3
+ "auto_map": {
4
+ "AutoConfig": "configuration_fc_clip.FCCLIPConfig",
5
+ "AutoModel": "modeling_fc_clip.FCCLIPForPanopticSegmentation"
6
+ },
7
+ "clip_model_name": "convnext_large_d_320",
8
+ "clip_pretrained": "laion2b_s29b_b131k_ft_soup",
9
+ "clip_embedding_dim": 768,
10
+ "conv_dim": 256,
11
+ "mask_dim": 256,
12
+ "transformer_dropout": 0.0,
13
+ "transformer_nheads": 8,
14
+ "transformer_dim_feedforward": 1024,
15
+ "transformer_enc_layers": 6,
16
+ "transformer_in_features": ["res3", "res4", "res5"],
17
+ "in_features": ["res2", "res3", "res4", "res5"],
18
+ "common_stride": 4,
19
+ "hidden_dim": 256,
20
+ "num_queries": 250,
21
+ "nheads": 8,
22
+ "dim_feedforward": 2048,
23
+ "dec_layers": 9,
24
+ "pre_norm": false,
25
+ "enforce_input_project": false,
26
+ "num_classes": 133,
27
+ "num_thing_classes": 80,
28
+ "geometric_ensemble_alpha": 0.4,
29
+ "geometric_ensemble_beta": 0.8,
30
+ "ensemble_on_valid_mask": false,
31
+ "object_mask_threshold": 0.8,
32
+ "overlap_threshold": 0.8,
33
+ "pixel_mean": [122.7709383, 116.7460125, 104.09373615],
34
+ "pixel_std": [68.5005327, 66.6321579, 70.32316305]
35
+ }
configuration_fc_clip.py ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """FCCLIPConfig — HuggingFace PretrainedConfig for FC-CLIP."""
2
+
3
+ from transformers import PretrainedConfig
4
+
5
+ # COCO panoptic 133 classes: things (0-79) then stuff (80-132)
6
+ COCO_PANOPTIC_CLASSES = [
7
+ # 80 thing classes
8
+ "person", "bicycle", "car", "motorcycle", "airplane", "bus", "train",
9
+ "truck", "boat", "traffic light", "fire hydrant", "stop sign",
10
+ "parking meter", "bench", "bird", "cat", "dog", "horse", "sheep", "cow",
11
+ "elephant", "bear", "zebra", "giraffe", "backpack", "umbrella", "handbag",
12
+ "tie", "suitcase", "frisbee", "skis", "snowboard", "sports ball", "kite",
13
+ "baseball bat", "baseball glove", "skateboard", "surfboard",
14
+ "tennis racket", "bottle", "wine glass", "cup", "fork", "knife", "spoon",
15
+ "bowl", "banana", "apple", "sandwich", "orange", "broccoli", "carrot",
16
+ "hot dog", "pizza", "donut", "cake", "chair", "couch", "potted plant",
17
+ "bed", "dining table", "toilet", "tv", "laptop", "mouse", "remote",
18
+ "keyboard", "cell phone", "microwave", "oven", "toaster", "sink",
19
+ "refrigerator", "book", "clock", "vase", "scissors", "teddy bear",
20
+ "hair drier", "toothbrush",
21
+ # 53 stuff classes
22
+ "banner", "blanket", "bridge", "cardboard", "counter", "curtain",
23
+ "door-stuff", "floor-wood", "flower", "fruit", "gravel", "house",
24
+ "light", "mirror-stuff", "net", "pillow", "platform", "playingfield",
25
+ "railroad", "river", "road", "roof", "sand", "sea", "shelf", "snow",
26
+ "stairs", "tent", "towel", "wall-brick", "wall-stone", "wall-tile",
27
+ "wall-wood", "water-other", "window-blind", "window-other",
28
+ "tree-merged", "fence-merged", "ceiling-merged", "sky-other-merged",
29
+ "cabinet-merged", "table-merged", "floor-other-merged",
30
+ "pavement-merged", "mountain-merged", "grass-merged", "dirt-merged",
31
+ "paper-merged", "food-other-merged", "building-other-merged",
32
+ "rock-merged", "wall-other-merged", "rug-merged",
33
+ ]
34
+
35
+
36
+ class FCCLIPConfig(PretrainedConfig):
37
+ model_type = "fc_clip"
38
+
39
+ def __init__(
40
+ self,
41
+ # CLIP backbone
42
+ clip_model_name="convnext_large_d_320",
43
+ clip_pretrained="laion2b_s29b_b131k_ft_soup",
44
+ clip_embedding_dim=768,
45
+ # pixel decoder
46
+ conv_dim=256,
47
+ mask_dim=256,
48
+ transformer_dropout=0.0,
49
+ transformer_nheads=8,
50
+ transformer_dim_feedforward=1024,
51
+ transformer_enc_layers=6,
52
+ transformer_in_features=("res3", "res4", "res5"),
53
+ in_features=("res2", "res3", "res4", "res5"),
54
+ common_stride=4,
55
+ # transformer decoder
56
+ hidden_dim=256,
57
+ num_queries=250,
58
+ nheads=8,
59
+ dim_feedforward=2048,
60
+ dec_layers=9,
61
+ pre_norm=False,
62
+ enforce_input_project=False,
63
+ # classification
64
+ num_classes=133,
65
+ num_thing_classes=80,
66
+ stuff_classes=None,
67
+ # FC-CLIP ensembling
68
+ geometric_ensemble_alpha=0.4,
69
+ geometric_ensemble_beta=0.8,
70
+ ensemble_on_valid_mask=False,
71
+ # inference thresholds
72
+ object_mask_threshold=0.8,
73
+ overlap_threshold=0.8,
74
+ # image normalisation (CLIP values, 0-255 scale, RGB)
75
+ pixel_mean=(122.7709383, 116.7460125, 104.09373615),
76
+ pixel_std=(68.5005327, 66.6321579, 70.32316305),
77
+ **kwargs,
78
+ ):
79
+ super().__init__(**kwargs)
80
+ self.clip_model_name = clip_model_name
81
+ self.clip_pretrained = clip_pretrained
82
+ self.clip_embedding_dim = clip_embedding_dim
83
+
84
+ self.conv_dim = conv_dim
85
+ self.mask_dim = mask_dim
86
+ self.transformer_dropout = transformer_dropout
87
+ self.transformer_nheads = transformer_nheads
88
+ self.transformer_dim_feedforward = transformer_dim_feedforward
89
+ self.transformer_enc_layers = transformer_enc_layers
90
+ self.transformer_in_features = list(transformer_in_features)
91
+ self.in_features = list(in_features)
92
+ self.common_stride = common_stride
93
+
94
+ self.hidden_dim = hidden_dim
95
+ self.num_queries = num_queries
96
+ self.nheads = nheads
97
+ self.dim_feedforward = dim_feedforward
98
+ self.dec_layers = dec_layers
99
+ self.pre_norm = pre_norm
100
+ self.enforce_input_project = enforce_input_project
101
+
102
+ self.num_classes = num_classes
103
+ self.num_thing_classes = num_thing_classes
104
+ self.stuff_classes = stuff_classes if stuff_classes is not None else COCO_PANOPTIC_CLASSES
105
+
106
+ self.geometric_ensemble_alpha = geometric_ensemble_alpha
107
+ self.geometric_ensemble_beta = geometric_ensemble_beta
108
+ self.ensemble_on_valid_mask = ensemble_on_valid_mask
109
+ self.object_mask_threshold = object_mask_threshold
110
+ self.overlap_threshold = overlap_threshold
111
+
112
+ self.pixel_mean = list(pixel_mean)
113
+ self.pixel_std = list(pixel_std)
convert_and_upload.py ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Convert FC-CLIP checkpoint and upload to HuggingFace Hub.
3
+
4
+ Run AFTER logging in:
5
+ huggingface-cli login
6
+
7
+ Then:
8
+ python convert_and_upload.py
9
+ """
10
+
11
+ import os
12
+ import sys
13
+
14
+ CHECKPOINT_PATH = os.path.expanduser(
15
+ "~/fiftyone/__models__/fcclip/models/fcclip_cocopan.pth"
16
+ )
17
+ HF_REPO_DIR = os.path.dirname(os.path.abspath(__file__))
18
+ HF_REPO_ID = "neerajaabhyankar/fc-clip"
19
+
20
+ # ---------------------------------------------------------------------------
21
+ # Step 1 — Convert .pth → model.safetensors
22
+ # ---------------------------------------------------------------------------
23
+
24
+ def convert():
25
+ import torch
26
+ from safetensors.torch import save_file
27
+
28
+ print(f"Loading checkpoint from {CHECKPOINT_PATH} ...")
29
+ ckpt = torch.load(CHECKPOINT_PATH, map_location="cpu", weights_only=False)
30
+ state_dict = ckpt.get("model", ckpt)
31
+
32
+ # Drop training-only keys (criterion.*) and backbone CLIP weights
33
+ # (backbone loads from open_clip at runtime; no need to store in safetensors)
34
+ skip_prefixes = ("criterion.", "backbone.")
35
+ filtered = {k: v for k, v in state_dict.items()
36
+ if not any(k.startswith(p) for p in skip_prefixes)}
37
+
38
+ print(f"Keeping {len(filtered)}/{len(state_dict)} keys (dropped backbone + criterion)")
39
+ for k, v in sorted(filtered.items()):
40
+ print(f" {k:80s} {tuple(v.shape)}")
41
+
42
+ out_path = os.path.join(HF_REPO_DIR, "model.safetensors")
43
+ save_file(filtered, out_path)
44
+ size_mb = os.path.getsize(out_path) / 1e6
45
+ print(f"\nSaved {out_path} ({size_mb:.1f} MB)")
46
+
47
+
48
+ # ---------------------------------------------------------------------------
49
+ # Step 2 — Upload folder to HF Hub
50
+ # ---------------------------------------------------------------------------
51
+
52
+ def upload():
53
+ from huggingface_hub import HfApi
54
+
55
+ api = HfApi()
56
+
57
+ print(f"\nCreating/verifying repo {HF_REPO_ID} ...")
58
+ try:
59
+ api.create_repo(HF_REPO_ID, repo_type="model", private=False, exist_ok=True)
60
+ print(" Repo ready.")
61
+ except Exception as e:
62
+ print(f" create_repo: {e} (may already exist, continuing)")
63
+
64
+ print(f"Uploading {HF_REPO_DIR} → {HF_REPO_ID} ...")
65
+ api.upload_folder(
66
+ folder_path=HF_REPO_DIR,
67
+ repo_id=HF_REPO_ID,
68
+ repo_type="model",
69
+ # Skip any local-only scripts/large intermediates
70
+ ignore_patterns=["*.pth", "__pycache__", "*.pyc", ".DS_Store"],
71
+ )
72
+ print(f"\nDone! https://huggingface.co/{HF_REPO_ID}")
73
+
74
+
75
+ if __name__ == "__main__":
76
+ safetensors_path = os.path.join(HF_REPO_DIR, "model.safetensors")
77
+ if not os.path.exists(safetensors_path):
78
+ convert()
79
+ else:
80
+ print(f"model.safetensors already exists at {safetensors_path}, skipping conversion.")
81
+ upload()
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:568d231866dde03197bd674a986d6a2bf1970306ea21d03567577e0ed4088b83
3
+ size 82928612
modeling_fc_clip.py ADDED
@@ -0,0 +1,1015 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """FC-CLIP: self-contained HuggingFace model (no detectron2).
2
+
3
+ Original paper: "A Simple Framework for Open-Vocabulary Segmentation and
4
+ Detection" (Yu et al., CVPR 2023). Original code: bytedance/fc-clip (MIT).
5
+
6
+ Architecture:
7
+ backbone — frozen OpenCLIP ConvNeXt-Large
8
+ pixel_decoder — 6-layer MSDA encoder + FPN (pure-Python MSDA fallback)
9
+ predictor — 5-layer Mask2Former cross-attention decoder
10
+ void_embedding — learnable background class embedding
11
+ """
12
+
13
+ import copy
14
+ import math
15
+ import warnings
16
+
17
+ import numpy as np
18
+ import torch
19
+ import torch.nn.functional as F
20
+ from torch import nn, Tensor
21
+ from torch.nn.init import xavier_uniform_, constant_, normal_
22
+
23
+ from transformers import PreTrainedModel
24
+ from .configuration_fc_clip import FCCLIPConfig
25
+
26
+ # ---------------------------------------------------------------------------
27
+ # VILD prompt templates (for open-vocabulary text classification)
28
+ # ---------------------------------------------------------------------------
29
+
30
+ VILD_PROMPT = [
31
+ "a photo of a {}.",
32
+ "This is a photo of a {}",
33
+ "There is a {} in the scene",
34
+ "There is the {} in the scene",
35
+ "a photo of a {} in the scene",
36
+ "a photo of a small {}.",
37
+ "a photo of a medium {}.",
38
+ "a photo of a large {}.",
39
+ "This is a photo of a small {}.",
40
+ "This is a photo of a medium {}.",
41
+ "This is a photo of a large {}.",
42
+ "There is a small {} in the scene.",
43
+ "There is a medium {} in the scene.",
44
+ "There is a large {} in the scene.",
45
+ ]
46
+
47
+ # ---------------------------------------------------------------------------
48
+ # Pure-Python Multi-Scale Deformable Attention
49
+ # (same maths as the CUDA op; CPU/GPU portable)
50
+ # ---------------------------------------------------------------------------
51
+
52
+ def _ms_deform_attn_core(value, value_spatial_shapes, sampling_locations, attention_weights):
53
+ N_, S_, M_, D_ = value.shape
54
+ _, Lq_, M_, L_, P_, _ = sampling_locations.shape
55
+ value_list = value.split([H_ * W_ for H_, W_ in value_spatial_shapes], dim=1)
56
+ sampling_grids = 2 * sampling_locations - 1
57
+ sampling_value_list = []
58
+ for lid_, (H_, W_) in enumerate(value_spatial_shapes):
59
+ value_l_ = value_list[lid_].flatten(2).transpose(1, 2).reshape(N_ * M_, D_, H_, W_)
60
+ sampling_grid_l_ = sampling_grids[:, :, :, lid_].transpose(1, 2).flatten(0, 1)
61
+ sampling_value_l_ = F.grid_sample(
62
+ value_l_, sampling_grid_l_, mode="bilinear", padding_mode="zeros", align_corners=False
63
+ )
64
+ sampling_value_list.append(sampling_value_l_)
65
+ attention_weights = attention_weights.transpose(1, 2).reshape(N_ * M_, 1, Lq_, L_ * P_)
66
+ output = (torch.stack(sampling_value_list, dim=-2).flatten(-2) * attention_weights).sum(-1).view(
67
+ N_, M_ * D_, Lq_
68
+ )
69
+ return output.transpose(1, 2).contiguous()
70
+
71
+
72
+ def _is_power_of_2(n):
73
+ return isinstance(n, int) and n > 0 and (n & (n - 1)) == 0
74
+
75
+
76
+ class MSDeformAttn(nn.Module):
77
+ def __init__(self, d_model=256, n_levels=4, n_heads=8, n_points=4):
78
+ super().__init__()
79
+ if d_model % n_heads != 0:
80
+ raise ValueError(f"d_model ({d_model}) must be divisible by n_heads ({n_heads})")
81
+ if not _is_power_of_2(d_model // n_heads):
82
+ warnings.warn("d_model // n_heads is not a power of 2; CUDA impl would be suboptimal")
83
+ self.d_model = d_model
84
+ self.n_levels = n_levels
85
+ self.n_heads = n_heads
86
+ self.n_points = n_points
87
+ self.sampling_offsets = nn.Linear(d_model, n_heads * n_levels * n_points * 2)
88
+ self.attention_weights = nn.Linear(d_model, n_heads * n_levels * n_points)
89
+ self.value_proj = nn.Linear(d_model, d_model)
90
+ self.output_proj = nn.Linear(d_model, d_model)
91
+ self._reset_parameters()
92
+
93
+ def _reset_parameters(self):
94
+ constant_(self.sampling_offsets.weight, 0.0)
95
+ thetas = torch.arange(self.n_heads, dtype=torch.float32) * (2.0 * math.pi / self.n_heads)
96
+ grid_init = torch.stack([thetas.cos(), thetas.sin()], -1)
97
+ grid_init = (grid_init / grid_init.abs().max(-1, keepdim=True)[0]).view(
98
+ self.n_heads, 1, 1, 2
99
+ ).repeat(1, self.n_levels, self.n_points, 1)
100
+ for i in range(self.n_points):
101
+ grid_init[:, :, i, :] *= i + 1
102
+ with torch.no_grad():
103
+ self.sampling_offsets.bias = nn.Parameter(grid_init.view(-1))
104
+ constant_(self.attention_weights.weight, 0.0)
105
+ constant_(self.attention_weights.bias, 0.0)
106
+ xavier_uniform_(self.value_proj.weight)
107
+ constant_(self.value_proj.bias, 0.0)
108
+ xavier_uniform_(self.output_proj.weight)
109
+ constant_(self.output_proj.bias, 0.0)
110
+
111
+ def forward(self, query, reference_points, input_flatten, input_spatial_shapes,
112
+ input_level_start_index, input_padding_mask=None):
113
+ N, Len_q, _ = query.shape
114
+ N, Len_in, _ = input_flatten.shape
115
+
116
+ value = self.value_proj(input_flatten)
117
+ if input_padding_mask is not None:
118
+ value = value.masked_fill(input_padding_mask[..., None], 0.0)
119
+ value = value.view(N, Len_in, self.n_heads, self.d_model // self.n_heads)
120
+
121
+ sampling_offsets = self.sampling_offsets(query).view(N, Len_q, self.n_heads, self.n_levels, self.n_points, 2)
122
+ attention_weights = self.attention_weights(query).view(N, Len_q, self.n_heads, self.n_levels * self.n_points)
123
+ attention_weights = F.softmax(attention_weights, -1).view(N, Len_q, self.n_heads, self.n_levels, self.n_points)
124
+
125
+ if reference_points.shape[-1] == 2:
126
+ offset_normalizer = torch.stack(
127
+ [input_spatial_shapes[..., 1], input_spatial_shapes[..., 0]], -1
128
+ )
129
+ sampling_locations = (
130
+ reference_points[:, :, None, :, None, :]
131
+ + sampling_offsets / offset_normalizer[None, None, None, :, None, :]
132
+ )
133
+ elif reference_points.shape[-1] == 4:
134
+ sampling_locations = (
135
+ reference_points[:, :, None, :, None, :2]
136
+ + sampling_offsets / self.n_points
137
+ * reference_points[:, :, None, :, None, 2:]
138
+ * 0.5
139
+ )
140
+ else:
141
+ raise ValueError(f"Last dim of reference_points must be 2 or 4, got {reference_points.shape[-1]}")
142
+
143
+ output = _ms_deform_attn_core(value, input_spatial_shapes, sampling_locations, attention_weights)
144
+ output = self.output_proj(output)
145
+ return output
146
+
147
+
148
+ # ---------------------------------------------------------------------------
149
+ # Positional encoding
150
+ # ---------------------------------------------------------------------------
151
+
152
+ class PositionEmbeddingSine(nn.Module):
153
+ def __init__(self, num_pos_feats=64, temperature=10000, normalize=False, scale=None):
154
+ super().__init__()
155
+ self.num_pos_feats = num_pos_feats
156
+ self.temperature = temperature
157
+ self.normalize = normalize
158
+ self.scale = 2 * math.pi if scale is None else scale
159
+
160
+ def forward(self, x, mask=None):
161
+ if mask is None:
162
+ mask = torch.zeros((x.size(0), x.size(2), x.size(3)), device=x.device, dtype=torch.bool)
163
+ not_mask = ~mask
164
+ y_embed = not_mask.cumsum(1, dtype=torch.float32)
165
+ x_embed = not_mask.cumsum(2, dtype=torch.float32)
166
+ if self.normalize:
167
+ eps = 1e-6
168
+ y_embed = y_embed / (y_embed[:, -1:, :] + eps) * self.scale
169
+ x_embed = x_embed / (x_embed[:, :, -1:] + eps) * self.scale
170
+ dim_t = torch.arange(self.num_pos_feats, dtype=torch.float32, device=x.device)
171
+ dim_t = self.temperature ** (2 * (dim_t // 2) / self.num_pos_feats)
172
+ pos_x = x_embed[:, :, :, None] / dim_t
173
+ pos_y = y_embed[:, :, :, None] / dim_t
174
+ pos_x = torch.stack((pos_x[:, :, :, 0::2].sin(), pos_x[:, :, :, 1::2].cos()), dim=4).flatten(3)
175
+ pos_y = torch.stack((pos_y[:, :, :, 0::2].sin(), pos_y[:, :, :, 1::2].cos()), dim=4).flatten(3)
176
+ return torch.cat((pos_y, pos_x), dim=3).permute(0, 3, 1, 2)
177
+
178
+
179
+ # ---------------------------------------------------------------------------
180
+ # Pixel decoder helpers
181
+ # ---------------------------------------------------------------------------
182
+
183
+ def _get_clones(module, N):
184
+ return nn.ModuleList([copy.deepcopy(module) for _ in range(N)])
185
+
186
+
187
+ def _get_activation_fn(activation):
188
+ if activation == "relu":
189
+ return F.relu
190
+ if activation == "gelu":
191
+ return F.gelu
192
+ raise RuntimeError(f"activation must be relu/gelu, not {activation}")
193
+
194
+
195
+ class Conv2dWithNorm(nn.Conv2d):
196
+ """nn.Conv2d + GroupNorm with detectron2-compatible state_dict keys.
197
+
198
+ State dict: ``{name}.weight``, ``{name}.norm.weight``, ``{name}.norm.bias``
199
+ (no conv bias — use_bias=False matches the pixel decoder's GN config).
200
+ """
201
+
202
+ def __init__(self, in_channels, out_channels, kernel_size, stride=1, padding=0, activation=None):
203
+ super().__init__(in_channels, out_channels, kernel_size, stride=stride, padding=padding, bias=False)
204
+ self.norm = nn.GroupNorm(32, out_channels)
205
+ self.activation = activation
206
+
207
+ def forward(self, x):
208
+ x = self.norm(super().forward(x))
209
+ if self.activation is not None:
210
+ x = self.activation(x)
211
+ return x
212
+
213
+
214
+ class MSDeformAttnTransformerEncoderLayer(nn.Module):
215
+ def __init__(self, d_model=256, d_ffn=1024, dropout=0.1, activation="relu",
216
+ n_levels=4, n_heads=8, n_points=4):
217
+ super().__init__()
218
+ self.self_attn = MSDeformAttn(d_model, n_levels, n_heads, n_points)
219
+ self.dropout1 = nn.Dropout(dropout)
220
+ self.norm1 = nn.LayerNorm(d_model)
221
+ self.linear1 = nn.Linear(d_model, d_ffn)
222
+ self.activation = _get_activation_fn(activation)
223
+ self.dropout2 = nn.Dropout(dropout)
224
+ self.linear2 = nn.Linear(d_ffn, d_model)
225
+ self.dropout3 = nn.Dropout(dropout)
226
+ self.norm2 = nn.LayerNorm(d_model)
227
+
228
+ @staticmethod
229
+ def with_pos_embed(tensor, pos):
230
+ return tensor if pos is None else tensor + pos
231
+
232
+ def forward_ffn(self, src):
233
+ src2 = self.linear2(self.dropout2(self.activation(self.linear1(src))))
234
+ src = src + self.dropout3(src2)
235
+ return self.norm2(src)
236
+
237
+ def forward(self, src, pos, reference_points, spatial_shapes, level_start_index, padding_mask=None):
238
+ src2 = self.self_attn(self.with_pos_embed(src, pos), reference_points, src,
239
+ spatial_shapes, level_start_index, padding_mask)
240
+ src = self.norm1(src + self.dropout1(src2))
241
+ return self.forward_ffn(src)
242
+
243
+
244
+ class MSDeformAttnTransformerEncoder(nn.Module):
245
+ def __init__(self, encoder_layer, num_layers):
246
+ super().__init__()
247
+ self.layers = _get_clones(encoder_layer, num_layers)
248
+ self.num_layers = num_layers
249
+
250
+ @staticmethod
251
+ def get_reference_points(spatial_shapes, valid_ratios, device):
252
+ reference_points_list = []
253
+ for lvl, (H_, W_) in enumerate(spatial_shapes):
254
+ ref_y, ref_x = torch.meshgrid(
255
+ torch.linspace(0.5, H_ - 0.5, H_, dtype=torch.float32, device=device),
256
+ torch.linspace(0.5, W_ - 0.5, W_, dtype=torch.float32, device=device),
257
+ indexing="ij",
258
+ )
259
+ ref_y = ref_y.reshape(-1)[None] / (valid_ratios[:, None, lvl, 1] * H_)
260
+ ref_x = ref_x.reshape(-1)[None] / (valid_ratios[:, None, lvl, 0] * W_)
261
+ reference_points_list.append(torch.stack((ref_x, ref_y), -1))
262
+ reference_points = torch.cat(reference_points_list, 1)
263
+ return reference_points[:, :, None] * valid_ratios[:, None]
264
+
265
+ def forward(self, src, spatial_shapes, level_start_index, valid_ratios, pos=None, padding_mask=None):
266
+ output = src
267
+ reference_points = self.get_reference_points(spatial_shapes, valid_ratios, device=src.device)
268
+ for layer in self.layers:
269
+ output = layer(output, pos, reference_points, spatial_shapes, level_start_index, padding_mask)
270
+ return output
271
+
272
+
273
+ class MSDeformAttnTransformerEncoderOnly(nn.Module):
274
+ def __init__(self, d_model=256, nhead=8, num_encoder_layers=6, dim_feedforward=1024,
275
+ dropout=0.1, activation="relu", num_feature_levels=4, enc_n_points=4):
276
+ super().__init__()
277
+ self.d_model = d_model
278
+ self.nhead = nhead
279
+ encoder_layer = MSDeformAttnTransformerEncoderLayer(
280
+ d_model, dim_feedforward, dropout, activation, num_feature_levels, nhead, enc_n_points
281
+ )
282
+ self.encoder = MSDeformAttnTransformerEncoder(encoder_layer, num_encoder_layers)
283
+ self.level_embed = nn.Parameter(torch.Tensor(num_feature_levels, d_model))
284
+ self._reset_parameters()
285
+
286
+ def _reset_parameters(self):
287
+ for p in self.parameters():
288
+ if p.dim() > 1:
289
+ nn.init.xavier_uniform_(p)
290
+ for m in self.modules():
291
+ if isinstance(m, MSDeformAttn):
292
+ m._reset_parameters()
293
+ normal_(self.level_embed)
294
+
295
+ def get_valid_ratio(self, mask):
296
+ _, H, W = mask.shape
297
+ valid_H = torch.sum(~mask[:, :, 0], 1)
298
+ valid_W = torch.sum(~mask[:, 0, :], 1)
299
+ return torch.stack([valid_W.float() / W, valid_H.float() / H], -1)
300
+
301
+ def forward(self, srcs, pos_embeds):
302
+ masks = [torch.zeros((x.size(0), x.size(2), x.size(3)), device=x.device, dtype=torch.bool) for x in srcs]
303
+ src_flatten, mask_flatten, lvl_pos_embed_flatten, spatial_shapes = [], [], [], []
304
+ for lvl, (src, mask, pos_embed) in enumerate(zip(srcs, masks, pos_embeds)):
305
+ bs, c, h, w = src.shape
306
+ spatial_shapes.append((h, w))
307
+ src = src.flatten(2).transpose(1, 2)
308
+ mask = mask.flatten(1)
309
+ pos_embed = pos_embed.flatten(2).transpose(1, 2)
310
+ lvl_pos_embed_flatten.append(pos_embed + self.level_embed[lvl].view(1, 1, -1))
311
+ src_flatten.append(src)
312
+ mask_flatten.append(mask)
313
+ src_flatten = torch.cat(src_flatten, 1)
314
+ mask_flatten = torch.cat(mask_flatten, 1)
315
+ lvl_pos_embed_flatten = torch.cat(lvl_pos_embed_flatten, 1)
316
+ spatial_shapes = torch.as_tensor(spatial_shapes, dtype=torch.long, device=src_flatten.device)
317
+ level_start_index = torch.cat((spatial_shapes.new_zeros((1,)), spatial_shapes.prod(1).cumsum(0)[:-1]))
318
+ valid_ratios = torch.stack([self.get_valid_ratio(m) for m in masks], 1)
319
+ memory = self.encoder(src_flatten, spatial_shapes, level_start_index, valid_ratios,
320
+ lvl_pos_embed_flatten, mask_flatten)
321
+ return memory, spatial_shapes, level_start_index
322
+
323
+
324
+ # ---------------------------------------------------------------------------
325
+ # Pixel Decoder
326
+ # ---------------------------------------------------------------------------
327
+
328
+ class MSDeformAttnPixelDecoder(nn.Module):
329
+ def __init__(self, config):
330
+ super().__init__()
331
+ # Feature channel sizes for ConvNeXt-Large: res2=192, res3=384, res4=768, res5=1536
332
+ feature_channels = {"res2": 192, "res3": 384, "res4": 768, "res5": 1536}
333
+ transformer_in_channels = [feature_channels[f] for f in config.transformer_in_features]
334
+
335
+ self.transformer_in_features = config.transformer_in_features # ["res3","res4","res5"]
336
+ self.transformer_num_feature_levels = len(self.transformer_in_features)
337
+ self.in_features = config.in_features # ["res2","res3","res4","res5"]
338
+ self.maskformer_num_feature_levels = 3
339
+ self.common_stride = config.common_stride
340
+
341
+ # min transformer stride = 8 (res3), num_fpn_levels = log2(8)-log2(4) = 1
342
+ self.num_fpn_levels = 1
343
+ conv_dim = config.conv_dim
344
+ mask_dim = config.mask_dim
345
+
346
+ # Input projections: res5 → res3 (reversed = high to low stride order)
347
+ input_proj_list = []
348
+ for in_ch in transformer_in_channels[::-1]:
349
+ input_proj_list.append(
350
+ nn.Sequential(
351
+ nn.Conv2d(in_ch, conv_dim, kernel_size=1),
352
+ nn.GroupNorm(32, conv_dim),
353
+ )
354
+ )
355
+ self.input_proj = nn.ModuleList(input_proj_list)
356
+ for proj in self.input_proj:
357
+ xavier_uniform_(proj[0].weight, gain=1)
358
+ constant_(proj[0].bias, 0)
359
+
360
+ self.transformer = MSDeformAttnTransformerEncoderOnly(
361
+ d_model=conv_dim,
362
+ dropout=config.transformer_dropout,
363
+ nhead=config.transformer_nheads,
364
+ dim_feedforward=config.transformer_dim_feedforward,
365
+ num_encoder_layers=config.transformer_enc_layers,
366
+ num_feature_levels=self.transformer_num_feature_levels,
367
+ )
368
+
369
+ N_steps = conv_dim // 2
370
+ self.pe_layer = PositionEmbeddingSine(N_steps, normalize=True)
371
+
372
+ self.mask_features = nn.Conv2d(conv_dim, mask_dim, kernel_size=1)
373
+ xavier_uniform_(self.mask_features.weight, gain=1)
374
+ constant_(self.mask_features.bias, 0)
375
+
376
+ # FPN lateral + output convs for res2 (1 level)
377
+ # Names adapter_1 / layer_1 match the detectron2 add_module pattern
378
+ self.adapter_1 = Conv2dWithNorm(feature_channels["res2"], conv_dim, kernel_size=1)
379
+ xavier_uniform_(self.adapter_1.weight, gain=1)
380
+ self.layer_1 = Conv2dWithNorm(conv_dim, conv_dim, kernel_size=3, padding=1, activation=F.relu)
381
+ xavier_uniform_(self.layer_1.weight, gain=1)
382
+
383
+ # Store as lists too (mirrors original lateral_convs / output_convs ordering)
384
+ self.lateral_convs = [self.adapter_1]
385
+ self.output_convs = [self.layer_1]
386
+
387
+ def forward_features(self, features):
388
+ srcs, pos = [], []
389
+ for idx, f in enumerate(self.transformer_in_features[::-1]):
390
+ x = features[f].float()
391
+ srcs.append(self.input_proj[idx](x))
392
+ pos.append(self.pe_layer(x))
393
+
394
+ y, spatial_shapes, level_start_index = self.transformer(srcs, pos)
395
+ bs = y.shape[0]
396
+
397
+ split_sizes = []
398
+ for i in range(self.transformer_num_feature_levels):
399
+ if i < self.transformer_num_feature_levels - 1:
400
+ split_sizes.append(int(level_start_index[i + 1] - level_start_index[i]))
401
+ else:
402
+ split_sizes.append(y.shape[1] - int(level_start_index[i]))
403
+ y = torch.split(y, split_sizes, dim=1)
404
+
405
+ out = []
406
+ for i, z in enumerate(y):
407
+ out.append(z.transpose(1, 2).view(bs, -1, int(spatial_shapes[i][0]), int(spatial_shapes[i][1])))
408
+
409
+ # FPN: add res2
410
+ fpn_features = [self.in_features[0]] # ["res2"]
411
+ for idx, f in enumerate(fpn_features[::-1]):
412
+ x = features[f].float()
413
+ cur_fpn = self.lateral_convs[idx](x)
414
+ merged = cur_fpn + F.interpolate(out[-1], size=cur_fpn.shape[-2:], mode="bilinear", align_corners=False)
415
+ out.append(self.output_convs[idx](merged))
416
+
417
+ multi_scale_features = []
418
+ for i, o in enumerate(out):
419
+ if i < self.maskformer_num_feature_levels:
420
+ multi_scale_features.append(o)
421
+
422
+ return self.mask_features(out[-1]), out[0], multi_scale_features
423
+
424
+
425
+ # ---------------------------------------------------------------------------
426
+ # Transformer decoder components
427
+ # ---------------------------------------------------------------------------
428
+
429
+ class MaskPooling(nn.Module):
430
+ def forward(self, x, mask):
431
+ if x.shape[-2:] != mask.shape[-2:]:
432
+ mask = F.interpolate(mask, size=x.shape[-2:], mode="bilinear", align_corners=False)
433
+ with torch.no_grad():
434
+ mask = (mask.detach() > 0).to(mask.dtype)
435
+ denorm = mask.sum(dim=(-1, -2), keepdim=True) + 1e-8
436
+ return torch.einsum("bchw,bqhw->bqc", x, mask / denorm)
437
+
438
+
439
+ class SelfAttentionLayer(nn.Module):
440
+ def __init__(self, d_model, nhead, dropout=0.0, normalize_before=False):
441
+ super().__init__()
442
+ self.self_attn = nn.MultiheadAttention(d_model, nhead, dropout=dropout)
443
+ self.norm = nn.LayerNorm(d_model)
444
+ self.dropout = nn.Dropout(dropout)
445
+ self.normalize_before = normalize_before
446
+ for p in self.parameters():
447
+ if p.dim() > 1:
448
+ nn.init.xavier_uniform_(p)
449
+
450
+ def with_pos_embed(self, tensor, pos):
451
+ return tensor if pos is None else tensor + pos
452
+
453
+ def forward(self, tgt, tgt_mask=None, tgt_key_padding_mask=None, query_pos=None):
454
+ if self.normalize_before:
455
+ tgt2 = self.norm(tgt)
456
+ q = k = self.with_pos_embed(tgt2, query_pos)
457
+ tgt2 = self.self_attn(q, k, value=tgt2, attn_mask=tgt_mask,
458
+ key_padding_mask=tgt_key_padding_mask)[0]
459
+ return tgt + self.dropout(tgt2)
460
+ q = k = self.with_pos_embed(tgt, query_pos)
461
+ tgt2 = self.self_attn(q, k, value=tgt, attn_mask=tgt_mask,
462
+ key_padding_mask=tgt_key_padding_mask)[0]
463
+ tgt = tgt + self.dropout(tgt2)
464
+ return self.norm(tgt)
465
+
466
+
467
+ class CrossAttentionLayer(nn.Module):
468
+ def __init__(self, d_model, nhead, dropout=0.0, normalize_before=False):
469
+ super().__init__()
470
+ self.multihead_attn = nn.MultiheadAttention(d_model, nhead, dropout=dropout)
471
+ self.norm = nn.LayerNorm(d_model)
472
+ self.dropout = nn.Dropout(dropout)
473
+ self.normalize_before = normalize_before
474
+ for p in self.parameters():
475
+ if p.dim() > 1:
476
+ nn.init.xavier_uniform_(p)
477
+
478
+ def with_pos_embed(self, tensor, pos):
479
+ return tensor if pos is None else tensor + pos
480
+
481
+ def forward(self, tgt, memory, memory_mask=None, memory_key_padding_mask=None,
482
+ pos=None, query_pos=None):
483
+ if self.normalize_before:
484
+ tgt2 = self.norm(tgt)
485
+ tgt2 = self.multihead_attn(
486
+ query=self.with_pos_embed(tgt2, query_pos),
487
+ key=self.with_pos_embed(memory, pos),
488
+ value=memory, attn_mask=memory_mask,
489
+ key_padding_mask=memory_key_padding_mask,
490
+ )[0]
491
+ return tgt + self.dropout(tgt2)
492
+ tgt2 = self.multihead_attn(
493
+ query=self.with_pos_embed(tgt, query_pos),
494
+ key=self.with_pos_embed(memory, pos),
495
+ value=memory, attn_mask=memory_mask,
496
+ key_padding_mask=memory_key_padding_mask,
497
+ )[0]
498
+ tgt = tgt + self.dropout(tgt2)
499
+ return self.norm(tgt)
500
+
501
+
502
+ class FFNLayer(nn.Module):
503
+ def __init__(self, d_model, dim_feedforward=2048, dropout=0.0, normalize_before=False):
504
+ super().__init__()
505
+ self.linear1 = nn.Linear(d_model, dim_feedforward)
506
+ self.dropout = nn.Dropout(dropout)
507
+ self.linear2 = nn.Linear(dim_feedforward, d_model)
508
+ self.norm = nn.LayerNorm(d_model)
509
+ self.normalize_before = normalize_before
510
+ for p in self.parameters():
511
+ if p.dim() > 1:
512
+ nn.init.xavier_uniform_(p)
513
+
514
+ def forward(self, tgt):
515
+ if self.normalize_before:
516
+ tgt2 = self.norm(tgt)
517
+ tgt2 = self.linear2(self.dropout(F.relu(self.linear1(tgt2))))
518
+ return tgt + self.dropout(tgt2)
519
+ tgt2 = self.linear2(self.dropout(F.relu(self.linear1(tgt))))
520
+ tgt = tgt + self.dropout(tgt2)
521
+ return self.norm(tgt)
522
+
523
+
524
+ class MLP(nn.Module):
525
+ def __init__(self, input_dim, hidden_dim, output_dim, num_layers):
526
+ super().__init__()
527
+ self.num_layers = num_layers
528
+ h = [hidden_dim] * (num_layers - 1)
529
+ self.layers = nn.ModuleList(
530
+ nn.Linear(n, k) for n, k in zip([input_dim] + h, h + [output_dim])
531
+ )
532
+
533
+ def forward(self, x):
534
+ for i, layer in enumerate(self.layers):
535
+ x = F.relu(layer(x)) if i < self.num_layers - 1 else layer(x)
536
+ return x
537
+
538
+
539
+ def get_classification_logits(x, text_classifier, logit_scale, num_templates):
540
+ x = F.normalize(x, dim=-1)
541
+ logit_scale = torch.clamp(logit_scale.exp(), max=100)
542
+ pred_logits = logit_scale * x @ text_classifier.T
543
+ final_pred_logits = []
544
+ cur_idx = 0
545
+ for num_t in num_templates:
546
+ final_pred_logits.append(pred_logits[:, :, cur_idx: cur_idx + num_t].max(-1).values)
547
+ cur_idx += num_t
548
+ final_pred_logits.append(pred_logits[:, :, -1])
549
+ return torch.stack(final_pred_logits, dim=-1)
550
+
551
+
552
+ # ---------------------------------------------------------------------------
553
+ # Transformer Decoder (Predictor)
554
+ # ---------------------------------------------------------------------------
555
+
556
+ class MultiScaleMaskedTransformerDecoder(nn.Module):
557
+ def __init__(self, config):
558
+ super().__init__()
559
+ hidden_dim = config.hidden_dim
560
+ num_queries = config.num_queries
561
+ nheads = config.nheads
562
+ dim_feedforward = config.dim_feedforward
563
+ dec_layers = config.dec_layers
564
+ pre_norm = config.pre_norm
565
+ mask_dim = config.mask_dim
566
+ clip_embedding_dim = config.clip_embedding_dim
567
+
568
+ N_steps = hidden_dim // 2
569
+ self.pe_layer = PositionEmbeddingSine(N_steps, normalize=True)
570
+
571
+ self.num_heads = nheads
572
+ self.num_layers = dec_layers
573
+ self.transformer_self_attention_layers = nn.ModuleList([
574
+ SelfAttentionLayer(hidden_dim, nheads, normalize_before=pre_norm)
575
+ for _ in range(dec_layers)
576
+ ])
577
+ self.transformer_cross_attention_layers = nn.ModuleList([
578
+ CrossAttentionLayer(hidden_dim, nheads, normalize_before=pre_norm)
579
+ for _ in range(dec_layers)
580
+ ])
581
+ self.transformer_ffn_layers = nn.ModuleList([
582
+ FFNLayer(hidden_dim, dim_feedforward, normalize_before=pre_norm)
583
+ for _ in range(dec_layers)
584
+ ])
585
+ self.decoder_norm = nn.LayerNorm(hidden_dim)
586
+
587
+ self.num_queries = num_queries
588
+ self.query_feat = nn.Embedding(num_queries, hidden_dim)
589
+ self.query_embed = nn.Embedding(num_queries, hidden_dim)
590
+
591
+ self.num_feature_levels = 3
592
+ self.level_embed = nn.Embedding(self.num_feature_levels, hidden_dim)
593
+ # in_channels = conv_dim = 256 = hidden_dim → empty Sequential (no projection needed)
594
+ self.input_proj = nn.ModuleList([nn.Sequential() for _ in range(self.num_feature_levels)])
595
+
596
+ self.mask_embed = MLP(hidden_dim, hidden_dim, mask_dim, 3)
597
+ self.mask_pooling = MaskPooling()
598
+ self._mask_pooling_proj = nn.Sequential(
599
+ nn.LayerNorm(hidden_dim),
600
+ nn.Linear(hidden_dim, hidden_dim),
601
+ )
602
+ self.class_embed = MLP(hidden_dim, hidden_dim, clip_embedding_dim, 3)
603
+ self.logit_scale = nn.Parameter(torch.ones([]) * np.log(1 / 0.07))
604
+
605
+ def forward(self, x, mask_features, text_classifier, num_templates, mask=None):
606
+ assert len(x) == self.num_feature_levels
607
+ src, pos, size_list = [], [], []
608
+ for i in range(self.num_feature_levels):
609
+ size_list.append(x[i].shape[-2:])
610
+ pos.append(self.pe_layer(x[i], None).flatten(2))
611
+ src.append(
612
+ self.input_proj[i](x[i]).flatten(2)
613
+ + self.level_embed.weight[i][None, :, None]
614
+ )
615
+ pos[-1] = pos[-1].permute(2, 0, 1)
616
+ src[-1] = src[-1].permute(2, 0, 1)
617
+
618
+ _, bs, _ = src[0].shape
619
+ query_embed = self.query_embed.weight.unsqueeze(1).repeat(1, bs, 1)
620
+ output = self.query_feat.weight.unsqueeze(1).repeat(1, bs, 1)
621
+
622
+ predictions_class, predictions_mask = [], []
623
+ outputs_class, outputs_mask, attn_mask = self._forward_prediction_heads(
624
+ output, mask_features, size_list[0], text_classifier, num_templates
625
+ )
626
+ predictions_class.append(outputs_class)
627
+ predictions_mask.append(outputs_mask)
628
+
629
+ for i in range(self.num_layers):
630
+ level_index = i % self.num_feature_levels
631
+ attn_mask[torch.where(attn_mask.sum(-1) == attn_mask.shape[-1])] = False
632
+
633
+ output = self.transformer_cross_attention_layers[i](
634
+ output, src[level_index],
635
+ memory_mask=attn_mask,
636
+ memory_key_padding_mask=None,
637
+ pos=pos[level_index], query_pos=query_embed,
638
+ )
639
+ output = self.transformer_self_attention_layers[i](
640
+ output, query_pos=query_embed
641
+ )
642
+ output = self.transformer_ffn_layers[i](output)
643
+
644
+ outputs_class, outputs_mask, attn_mask = self._forward_prediction_heads(
645
+ output, mask_features,
646
+ size_list[(i + 1) % self.num_feature_levels],
647
+ text_classifier, num_templates,
648
+ )
649
+ predictions_class.append(outputs_class)
650
+ predictions_mask.append(outputs_mask)
651
+
652
+ return {
653
+ "pred_logits": predictions_class[-1],
654
+ "pred_masks": predictions_mask[-1],
655
+ }
656
+
657
+ def _forward_prediction_heads(self, output, mask_features, attn_mask_target_size,
658
+ text_classifier, num_templates):
659
+ decoder_output = self.decoder_norm(output).transpose(0, 1)
660
+ mask_embed = self.mask_embed(decoder_output)
661
+ outputs_mask = torch.einsum("bqc,bchw->bqhw", mask_embed, mask_features)
662
+
663
+ maskpool_embeddings = self.mask_pooling(x=mask_features, mask=outputs_mask)
664
+ maskpool_embeddings = self._mask_pooling_proj(maskpool_embeddings)
665
+ class_embed = self.class_embed(maskpool_embeddings + decoder_output)
666
+ outputs_class = get_classification_logits(class_embed, text_classifier, self.logit_scale, num_templates)
667
+
668
+ attn_mask = F.interpolate(outputs_mask, size=attn_mask_target_size, mode="bilinear", align_corners=False)
669
+ attn_mask = (
670
+ attn_mask.sigmoid().flatten(2).unsqueeze(1).repeat(1, self.num_heads, 1, 1).flatten(0, 1) < 0.5
671
+ ).bool().detach()
672
+ return outputs_class, outputs_mask, attn_mask
673
+
674
+
675
+ # ---------------------------------------------------------------------------
676
+ # FC-CLIP Head (connects pixel decoder + transformer decoder)
677
+ # ---------------------------------------------------------------------------
678
+
679
+ class FCCLIPHead(nn.Module):
680
+ def __init__(self, config):
681
+ super().__init__()
682
+ self.pixel_decoder = MSDeformAttnPixelDecoder(config)
683
+ self.predictor = MultiScaleMaskedTransformerDecoder(config)
684
+
685
+ def forward(self, features):
686
+ mask_features, _, multi_scale_features = self.pixel_decoder.forward_features(features)
687
+ return self.predictor(
688
+ multi_scale_features, mask_features,
689
+ text_classifier=features["text_classifier"],
690
+ num_templates=features["num_templates"],
691
+ )
692
+
693
+
694
+ # ---------------------------------------------------------------------------
695
+ # CLIP Backbone (frozen)
696
+ # ---------------------------------------------------------------------------
697
+
698
+ class CLIPBackbone(nn.Module):
699
+ """Frozen OpenCLIP ConvNeXt-Large backbone."""
700
+
701
+ def __init__(self, model_name, pretrained):
702
+ super().__init__()
703
+ import open_clip
704
+ self.clip_model, _, _ = open_clip.create_model_and_transforms(model_name, pretrained=pretrained)
705
+ self.text_tokenizer = open_clip.get_tokenizer(model_name)
706
+ self._freeze()
707
+
708
+ def _freeze(self):
709
+ self.eval()
710
+ for p in self.clip_model.parameters():
711
+ p.requires_grad = False
712
+
713
+ @property
714
+ def dim_latent(self):
715
+ return self.clip_model.text_projection.shape[-1]
716
+
717
+ def forward(self, x):
718
+ self.eval()
719
+ with torch.no_grad():
720
+ return self._extract_features_convnext(x)
721
+
722
+ def _extract_features_convnext(self, x):
723
+ out = {}
724
+ x = self.clip_model.visual.trunk.stem(x)
725
+ out["stem"] = x.contiguous()
726
+ for i in range(4):
727
+ x = self.clip_model.visual.trunk.stages[i](x)
728
+ out[f"res{i+2}"] = x.contiguous()
729
+ out["clip_vis_dense"] = self.clip_model.visual.trunk.norm_pre(x).contiguous()
730
+ return out
731
+
732
+ def visual_prediction_forward(self, x, masks=None):
733
+ batch, num_query, channel = x.shape
734
+ x = x.reshape(batch * num_query, channel, 1, 1)
735
+ x = self.clip_model.visual.trunk.head(x)
736
+ x = self.clip_model.visual.head(x)
737
+ return x.view(batch, num_query, x.shape[-1])
738
+
739
+ def encode_text(self, text, normalize=False):
740
+ cast_dtype = self.clip_model.transformer.get_cast_dtype()
741
+ x = self.clip_model.token_embedding(text).to(cast_dtype)
742
+ x = x + self.clip_model.positional_embedding.to(cast_dtype)
743
+ # Handle both old (LND) and new (NLD, batch_first=True) open_clip conventions
744
+ transformer = self.clip_model.transformer
745
+ if getattr(transformer, "batch_first", False):
746
+ x = transformer(x, attn_mask=self.clip_model.attn_mask)
747
+ else:
748
+ x = x.permute(1, 0, 2)
749
+ x = transformer(x, attn_mask=self.clip_model.attn_mask)
750
+ x = x.permute(1, 0, 2)
751
+ x = self.clip_model.ln_final(x)
752
+ x = x[torch.arange(x.shape[0]), text.argmax(dim=-1)] @ self.clip_model.text_projection
753
+ return F.normalize(x, dim=-1) if normalize else x
754
+
755
+ def get_text_classifier(self, text_list, device):
756
+ self.eval()
757
+ with torch.no_grad():
758
+ tokens = self.text_tokenizer(text_list).to(device)
759
+ return self.encode_text(tokens, normalize=False)
760
+
761
+
762
+ # ---------------------------------------------------------------------------
763
+ # Main PreTrainedModel
764
+ # ---------------------------------------------------------------------------
765
+
766
+ class FCCLIPForPanopticSegmentation(PreTrainedModel):
767
+ """FC-CLIP panoptic segmentation model (HuggingFace trust_remote_code).
768
+
769
+ Usage::
770
+
771
+ from transformers import AutoModel
772
+ model = AutoModel.from_pretrained("neerajaabhyankar/fc-clip", trust_remote_code=True)
773
+ # pixel_values: float32 RGB tensor [B, 3, H, W], normalised with CLIP means/stds
774
+ panoptic_seg, segments_info = model(pixel_values)[0]
775
+ """
776
+
777
+ config_class = FCCLIPConfig
778
+
779
+ def __init__(self, config: FCCLIPConfig):
780
+ super().__init__(config)
781
+ self.backbone = CLIPBackbone(config.clip_model_name, config.clip_pretrained)
782
+ self.sem_seg_head = FCCLIPHead(config)
783
+ self.mask_pooling = MaskPooling()
784
+ self.void_embedding = nn.Embedding(1, config.clip_embedding_dim)
785
+
786
+ # Registered buffers for normalisation
787
+ self.register_buffer(
788
+ "pixel_mean",
789
+ torch.tensor(config.pixel_mean).view(-1, 1, 1),
790
+ persistent=False,
791
+ )
792
+ self.register_buffer(
793
+ "pixel_std",
794
+ torch.tensor(config.pixel_std).view(-1, 1, 1),
795
+ persistent=False,
796
+ )
797
+
798
+ # Cache text classifiers per class list
799
+ self._test_text_classifier = None
800
+ self._test_num_templates = None
801
+
802
+ # Build category overlap mask (all overlap since train==test for COCO)
803
+ num_cls = config.num_classes
804
+ self.register_buffer(
805
+ "category_overlapping_mask",
806
+ torch.ones(num_cls, dtype=torch.long),
807
+ persistent=False,
808
+ )
809
+
810
+ # ------------------------------------------------------------------
811
+ # Text classifier helpers
812
+ # ------------------------------------------------------------------
813
+
814
+ def _build_text_classifier(self, class_names, device):
815
+ """Encode class_names with VILD templates, average, and normalise."""
816
+ templated = []
817
+ num_templates = []
818
+ for synonyms in class_names:
819
+ if isinstance(synonyms, str):
820
+ synonyms = [synonyms]
821
+ for syn in synonyms:
822
+ for tmpl in VILD_PROMPT:
823
+ templated.append(tmpl.format(syn))
824
+ num_templates.append(len(synonyms))
825
+
826
+ bs = 128
827
+ feats = []
828
+ for i in range(0, len(templated), bs):
829
+ feats.append(self.backbone.get_text_classifier(templated[i: i + bs], device).detach())
830
+ text_classifier = torch.cat(feats, 0)
831
+
832
+ text_classifier = text_classifier / text_classifier.norm(dim=-1, keepdim=True)
833
+ n_per = len(VILD_PROMPT)
834
+ text_classifier = text_classifier.reshape(
835
+ text_classifier.shape[0] // n_per, n_per, text_classifier.shape[-1]
836
+ ).mean(1)
837
+ text_classifier = text_classifier / text_classifier.norm(dim=-1, keepdim=True)
838
+ return text_classifier, [1] * len(class_names)
839
+
840
+ def get_text_classifier(self, class_names=None, device=None):
841
+ if device is None:
842
+ device = self.pixel_mean.device
843
+ if class_names is None:
844
+ class_names = self.config.stuff_classes
845
+
846
+ if self._test_text_classifier is None:
847
+ tc, nt = self._build_text_classifier(class_names, device)
848
+ self._test_text_classifier = tc
849
+ self._test_num_templates = nt
850
+
851
+ return self._test_text_classifier, self._test_num_templates
852
+
853
+ def set_class_names(self, class_names):
854
+ """Override vocabulary for open-vocabulary inference."""
855
+ self._test_text_classifier = None
856
+ self._test_num_templates = None
857
+ self.config.stuff_classes = class_names
858
+ self.config.num_classes = len(class_names)
859
+ self.config.num_thing_classes = 0 # treat all as stuff in custom mode
860
+
861
+ # ------------------------------------------------------------------
862
+ # Forward
863
+ # ------------------------------------------------------------------
864
+
865
+ def forward(self, pixel_values, class_names=None):
866
+ """
867
+ Args:
868
+ pixel_values: float32 RGB tensor [B, 3, H, W] already normalised with CLIP
869
+ means/stds (or raw [0,255] images — call preprocess_image first).
870
+ class_names: optional list of class name strings to override vocabulary.
871
+
872
+ Returns:
873
+ list of (panoptic_seg, segments_info) tuples, one per image in batch.
874
+ panoptic_seg: int32 tensor [H, W] — pixel → segment id
875
+ segments_info: list[dict] with keys "id", "category_id", "isthing"
876
+ """
877
+ if class_names is not None:
878
+ self.set_class_names(class_names)
879
+
880
+ images = pixel_values
881
+ h_pad, w_pad = images.shape[-2:]
882
+
883
+ # Run frozen backbone
884
+ features = self.backbone(images)
885
+
886
+ # Text classifier
887
+ text_classifier, num_templates = self.get_text_classifier(device=images.device)
888
+ text_classifier = text_classifier.to(images.device)
889
+ text_classifier = torch.cat(
890
+ [text_classifier, F.normalize(self.void_embedding.weight, dim=-1)], dim=0
891
+ )
892
+ features["text_classifier"] = text_classifier
893
+ features["num_templates"] = num_templates
894
+
895
+ # Segmentation head
896
+ outputs = self.sem_seg_head(features)
897
+ mask_cls_results = outputs["pred_logits"]
898
+ mask_pred_results = outputs["pred_masks"]
899
+
900
+ # Geometric ensemble (in-vocab + out-vocab)
901
+ clip_feature = features["clip_vis_dense"]
902
+ mask_for_pooling = F.interpolate(
903
+ mask_pred_results, size=clip_feature.shape[-2:], mode="bilinear", align_corners=False
904
+ )
905
+ pooled_clip_feature = self.mask_pooling(clip_feature, mask_for_pooling)
906
+ pooled_clip_feature = self.backbone.visual_prediction_forward(pooled_clip_feature)
907
+ out_vocab_cls_results = get_classification_logits(
908
+ pooled_clip_feature, text_classifier, self.backbone.clip_model.logit_scale, num_templates
909
+ )
910
+
911
+ in_vocab_cls_results = mask_cls_results[..., :-1]
912
+ out_vocab_cls_results = out_vocab_cls_results[..., :-1]
913
+ out_vocab_cls_probs = out_vocab_cls_results.softmax(-1)
914
+ in_vocab_cls_results = in_vocab_cls_results.softmax(-1)
915
+
916
+ cat_overlap = self.category_overlapping_mask.to(images.device)
917
+ alpha = self.config.geometric_ensemble_alpha
918
+ beta = self.config.geometric_ensemble_beta
919
+
920
+ cls_logits_seen = (
921
+ (in_vocab_cls_results ** (1 - alpha) * out_vocab_cls_probs ** alpha).log()
922
+ * cat_overlap
923
+ )
924
+ cls_logits_unseen = (
925
+ (in_vocab_cls_results ** (1 - beta) * out_vocab_cls_probs ** beta).log()
926
+ * (1 - cat_overlap)
927
+ )
928
+ cls_results = cls_logits_seen + cls_logits_unseen
929
+
930
+ is_void_prob = F.softmax(mask_cls_results, dim=-1)[..., -1:]
931
+ mask_cls_probs = torch.cat([
932
+ cls_results.softmax(-1) * (1.0 - is_void_prob),
933
+ is_void_prob,
934
+ ], dim=-1)
935
+ mask_cls_results = torch.log(mask_cls_probs + 1e-8)
936
+
937
+ # Upsample masks to input resolution
938
+ mask_pred_results = F.interpolate(
939
+ mask_pred_results,
940
+ size=(h_pad, w_pad),
941
+ mode="bilinear",
942
+ align_corners=False,
943
+ )
944
+
945
+ results = []
946
+ for mask_cls_result, mask_pred_result in zip(mask_cls_results, mask_pred_results):
947
+ panoptic_r = self.panoptic_inference(mask_cls_result, mask_pred_result)
948
+ results.append(panoptic_r)
949
+ return results
950
+
951
+ def panoptic_inference(self, mask_cls, mask_pred):
952
+ scores, labels = F.softmax(mask_cls, dim=-1).max(-1)
953
+ mask_pred = mask_pred.sigmoid()
954
+ num_classes = self.config.num_classes
955
+ thing_ids = set(range(self.config.num_thing_classes))
956
+
957
+ keep = labels.ne(num_classes) & (scores > self.config.object_mask_threshold)
958
+ cur_scores = scores[keep]
959
+ cur_classes = labels[keep]
960
+ cur_masks = mask_pred[keep]
961
+
962
+ h, w = cur_masks.shape[-2:]
963
+ panoptic_seg = torch.zeros((h, w), dtype=torch.int32, device=cur_masks.device)
964
+ segments_info = []
965
+
966
+ if cur_masks.shape[0] == 0:
967
+ return panoptic_seg, segments_info
968
+
969
+ cur_prob_masks = cur_scores.view(-1, 1, 1) * cur_masks
970
+ cur_mask_ids = cur_prob_masks.argmax(0)
971
+ stuff_memory = {}
972
+ current_segment_id = 0
973
+
974
+ for k in range(cur_classes.shape[0]):
975
+ pred_class = cur_classes[k].item()
976
+ isthing = pred_class in thing_ids
977
+ mask_area = (cur_mask_ids == k).sum().item()
978
+ original_area = (cur_masks[k] >= 0.5).sum().item()
979
+ mask = (cur_mask_ids == k) & (cur_masks[k] >= 0.5)
980
+
981
+ if mask_area > 0 and original_area > 0 and mask.sum().item() > 0:
982
+ if mask_area / original_area < self.config.overlap_threshold:
983
+ continue
984
+ if not isthing:
985
+ if pred_class in stuff_memory:
986
+ panoptic_seg[mask] = stuff_memory[pred_class]
987
+ continue
988
+ else:
989
+ stuff_memory[pred_class] = current_segment_id + 1
990
+
991
+ current_segment_id += 1
992
+ panoptic_seg[mask] = current_segment_id
993
+ segments_info.append({
994
+ "id": current_segment_id,
995
+ "isthing": bool(isthing),
996
+ "category_id": int(pred_class),
997
+ })
998
+
999
+ return panoptic_seg, segments_info
1000
+
1001
+ # ------------------------------------------------------------------
1002
+ # Convenience preprocessing
1003
+ # ------------------------------------------------------------------
1004
+
1005
+ def preprocess_image(self, image):
1006
+ """Normalise a uint8 RGB numpy array or PIL image to a model-ready tensor.
1007
+
1008
+ Returns float32 tensor [1, 3, H, W] on the same device as the model.
1009
+ """
1010
+ import numpy as np
1011
+ if not isinstance(image, np.ndarray):
1012
+ image = np.array(image.convert("RGB"))
1013
+ tensor = torch.from_numpy(image).float().permute(2, 0, 1).unsqueeze(0)
1014
+ tensor = tensor.to(self.pixel_mean.device)
1015
+ return (tensor - self.pixel_mean) / self.pixel_std
preprocessor_config.json ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "image_processor_type": "BaseImageProcessor",
3
+ "do_normalize": true,
4
+ "image_mean": [0.48145466, 0.4578275, 0.40821073],
5
+ "image_std": [0.26862954, 0.26130258, 0.27577711],
6
+ "do_rescale": true,
7
+ "rescale_factor": 0.00392156862745098,
8
+ "do_resize": false,
9
+ "size": {"height": null, "width": null}
10
+ }