Omar Alama commited on
Commit
72374cb
·
0 Parent(s):
.gitattributes ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ *.jpg filter=lfs diff=lfs merge=lfs -text
README.md ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: RADSeg
3
+ emoji: 📚
4
+ colorFrom: gray
5
+ colorTo: blue
6
+ sdk: gradio
7
+ sdk_version: 6.3.0
8
+ python_version: '3.11'
9
+ app_file: radseg_demo.py
10
+ pinned: true
11
+ license: mit
12
+ short_description: Fast & light zero-shot open vocabulary semantic segmentation
13
+ ---
14
+
15
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
assets/example1.jpg ADDED

Git LFS Details

  • SHA256: 08bf5a984b3372dac665a62cf8a8b80806bd024907549bc014e7ce8e5d6e0358
  • Pointer size: 130 Bytes
  • Size of remote file: 91.2 kB
assets/example2.jpg ADDED

Git LFS Details

  • SHA256: 6d3b83e406f57dcc8786b1e2260a26370be421be63fc9282dd4121df0f765c91
  • Pointer size: 131 Bytes
  • Size of remote file: 211 kB
assets/example3.jpg ADDED

Git LFS Details

  • SHA256: 032d953137cc7392c115f647875c6a97161fd4ec1015a8c03a7494bc6c724d7f
  • Pointer size: 131 Bytes
  • Size of remote file: 233 kB
radseg/base.py ADDED
@@ -0,0 +1,216 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import abc
2
+ from typing_extensions import Tuple, List, override
3
+ import torch
4
+
5
+ from radseg.prompt_templates import openai_imagenet_template
6
+
7
+ class ImageEncoder(abc.ABC):
8
+ """Interface for all image encoders"""
9
+
10
+ def __init__(self, device = None):
11
+ if device is None:
12
+ self.device = "cuda" if torch.cuda.is_available() else "cpu"
13
+ else:
14
+ self.device = device
15
+
16
+ @abc.abstractmethod
17
+ def is_compatible_size(self, h: int, w: int) -> bool:
18
+ """Checks if height and width of image is compatible with encoder"""
19
+ pass
20
+
21
+ @abc.abstractmethod
22
+ def get_nearest_size(self, h, w) -> Tuple[int, int]:
23
+ """Returns nearest compatible size as (h,w) tuple"""
24
+ pass
25
+
26
+ class ImageGlobalEncoder(ImageEncoder):
27
+ """Interface for all image level (global) encoders"""
28
+
29
+ @abc.abstractmethod
30
+ def encode_image_to_vector(
31
+ self, rgb_image: torch.FloatTensor) -> torch.FloatTensor:
32
+ """Encode rgb image into an image level feature vector
33
+
34
+ Args:
35
+ rgb_image: Bx3xHxW float tensor representing an image with values in [0-1]
36
+ Returns:
37
+ BxC float tensor representing global features.
38
+ """
39
+
40
+ class ImageSpatialEncoder(ImageEncoder):
41
+ """Interface for all pixel level / patch level encoders"""
42
+
43
+ @abc.abstractmethod
44
+ def encode_image_to_feat_map(
45
+ self, rgb_image: torch.FloatTensor) -> torch.FloatTensor:
46
+ """Encode rgb image into a feature map
47
+
48
+ Args:
49
+ rgb_image: Bx3xHxW float tensor representing an image with values in [0-1]
50
+ Returns:
51
+ BxCxH'xW' float tensor representing pixel / patch level features.
52
+ """
53
+
54
+ class ImageSpatialGlobalEncoder(ImageSpatialEncoder, ImageGlobalEncoder):
55
+ "Interface for encoders that implement both global and spatial encoding APIs"
56
+
57
+ @abc.abstractmethod
58
+ def encode_image_to_feat_map_and_vector(self, rgb_image: torch.FloatTensor) \
59
+ -> Tuple[torch.FloatTensor, torch.FloatTensor]:
60
+ """Encode rgb image into a feature map and an image level feature vector
61
+
62
+ Args:
63
+ rgb_image: Bx3xHxW float tensor representing an image with values in [0-1]
64
+ Returns:
65
+ A tuple of
66
+ - BxCxH'xW' float tensor representing pixel / patch level features.
67
+ - BxC float tensor representing global features.
68
+ """
69
+
70
+ class LangImageEncoder(ImageEncoder):
71
+ """Interface for all image encoders that produce language aligned features"""
72
+
73
+ def __init__(self, device = None):
74
+ super().__init__(device)
75
+ self.prompt_templates = openai_imagenet_template
76
+
77
+ def insert_labels_into_templates(self, labels: List[str]) -> List[List[str]]:
78
+ """Inserts each labels into a set of stored templates.
79
+
80
+ Args:
81
+ labels: A list of length T of class names / labels. Ex. ['cat', 'dog']
82
+ Returns:
83
+ labeled_templates as a list of length T of a list of length P of strings.
84
+ where T is the number of labels and P is the number of templates.
85
+ """
86
+ return [[pt(x) for pt in self.prompt_templates] for x in labels]
87
+
88
+ @abc.abstractmethod
89
+ def encode_labels(self, labels: List[str]) -> torch.FloatTensor:
90
+ """Encodes a list of labels into feature vectors using template prompts
91
+
92
+ Args:
93
+ labels: A list of length T of class names / labels. Ex. ['cat', 'dog']
94
+ Returns:
95
+ A TxD float tensor where T represents the number of classes and D
96
+ represents the feature space dimension.
97
+ """
98
+ pass
99
+
100
+ @abc.abstractmethod
101
+ def encode_prompts(self, prompts: List[str]) -> torch.FloatTensor:
102
+ """Encodes a list of prompts into feature vectors directly
103
+
104
+ Args:
105
+ prompts: A list of strings of arbitrary length of prompts to encode.
106
+ Returns:
107
+ A TxD float tensor where T represents the number of prompts and D
108
+ represents the feature space dimension.
109
+ """
110
+ pass
111
+
112
+ class LangSpatialImageEncoder(LangImageEncoder, ImageSpatialEncoder):
113
+
114
+ @abc.abstractmethod
115
+ def align_spatial_features_with_language(
116
+ self, features: torch.FloatTensor) -> torch.FloatTensor:
117
+ """Projects image features to the language space if it is not already there.
118
+
119
+ Some encoders directly output the language aligned features in which case
120
+ this function will simply be a pass through.
121
+
122
+ Args:
123
+ features: A (BxCxHxW) float tensor representing spatial features obtained
124
+ from encode_image_to_feat_map.
125
+ Returns:
126
+ A (BxDxHxW) float tensor that is in the same feature space as the text
127
+ embeddings obtained from encode_labels or encode_prompts.
128
+ """
129
+ pass
130
+
131
+ class LangGlobalImageEncoder(LangImageEncoder, ImageGlobalEncoder):
132
+
133
+ @abc.abstractmethod
134
+ def align_global_features_with_language(
135
+ self, features: torch.FloatTensor) -> torch.FloatTensor:
136
+ """Projects image global features to the language space if needed
137
+
138
+ Some encoders directly output the language aligned features in which case
139
+ this function will simply be a pass through.
140
+
141
+ Args:
142
+ features: A (BxC) float tensor representing global features obtained
143
+ from encode_image_to_vector.
144
+ Returns:
145
+ A (BxD) float tensor that is in the same feature space as the text
146
+ embeddings obtained from encode_labels or encode_prompts.
147
+ """
148
+ pass
149
+
150
+
151
+ class LangSpatialGlobalImageEncoder(LangGlobalImageEncoder,
152
+ LangSpatialImageEncoder):
153
+ pass
154
+
155
+
156
+ class ImageSemSegEncoder(LangSpatialImageEncoder):
157
+ """Interface for encoders that are actually semantic segmentors.
158
+
159
+ To avoid rewriting major portions of the codebase. We consider semantic
160
+ segmentation as simply a onehot encoder but nevertheless still producing a
161
+ feature vector that can be aggregated and compared with using cos-sim...etc.
162
+ Would be more efficient to deal with integers of course but we create this
163
+ for convenience. This way these "encoders" can be used with existing mappers.
164
+ """
165
+
166
+ def __init__(self, device = None):
167
+ super().__init__(device)
168
+ self.eps = 1e-10
169
+
170
+ @property
171
+ @abc.abstractmethod
172
+ def num_classes(self) -> int:
173
+ pass
174
+
175
+ @property
176
+ @abc.abstractmethod
177
+ def cat_index_to_name(self):
178
+ """Returns a mapping from category index to category name.
179
+
180
+ cat_index_to_name[cat_index] gives the name of that category index.
181
+ """
182
+ pass
183
+
184
+ @property
185
+ @abc.abstractmethod
186
+ def cat_name_to_index(self):
187
+ """Returns a mapping from category index to category name.
188
+
189
+ cat_name_to_index[cat_name] gives the one hot index of that category name.
190
+ """
191
+ pass
192
+
193
+ @override
194
+ def encode_labels(self, labels: List[str]) -> torch.FloatTensor:
195
+ """Encodes a list of category names into onehot vectors using stored mapping
196
+
197
+ Args:
198
+ labels: A list of length T of category names / labels. Ex. ['cat', 'dog']
199
+ Returns:
200
+ A TxD float tensor where T represents the number of classes and D
201
+ represents the feature space dimension.
202
+ """
203
+ onehot = torch.full((len(labels), self.num_classes), self.eps,
204
+ dtype=torch.float, device=self.device)
205
+ for i,c in enumerate(labels):
206
+ onehot[i, self.cat_name_to_index[c]] = 1
207
+ return onehot
208
+
209
+ @override
210
+ def encode_prompts(self, prompts: List[str]) -> torch.FloatTensor:
211
+ """alias for encode_labels"""
212
+ return self.encode_labels(prompts)
213
+
214
+ @override
215
+ def align_spatial_features_with_language(self, features):
216
+ return features
radseg/prompt_templates.py ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Includes common prompt templates for encoding labels"""
2
+
3
+ openai_imagenet_template = [
4
+ lambda c: f'a bad photo of a {c}.',
5
+ lambda c: f'a photo of many {c}.',
6
+ lambda c: f'a sculpture of a {c}.',
7
+ lambda c: f'a photo of the hard to see {c}.',
8
+ lambda c: f'a low resolution photo of the {c}.',
9
+ lambda c: f'a rendering of a {c}.',
10
+ lambda c: f'graffiti of a {c}.',
11
+ lambda c: f'a bad photo of the {c}.',
12
+ lambda c: f'a cropped photo of the {c}.',
13
+ lambda c: f'a tattoo of a {c}.',
14
+ lambda c: f'the embroidered {c}.',
15
+ lambda c: f'a photo of a hard to see {c}.',
16
+ lambda c: f'a bright photo of a {c}.',
17
+ lambda c: f'a photo of a clean {c}.',
18
+ lambda c: f'a photo of a dirty {c}.',
19
+ lambda c: f'a dark photo of the {c}.',
20
+ lambda c: f'a drawing of a {c}.',
21
+ lambda c: f'a photo of my {c}.',
22
+ lambda c: f'the plastic {c}.',
23
+ lambda c: f'a photo of the cool {c}.',
24
+ lambda c: f'a close-up photo of a {c}.',
25
+ lambda c: f'a black and white photo of the {c}.',
26
+ lambda c: f'a painting of the {c}.',
27
+ lambda c: f'a painting of a {c}.',
28
+ lambda c: f'a pixelated photo of the {c}.',
29
+ lambda c: f'a sculpture of the {c}.',
30
+ lambda c: f'a bright photo of the {c}.',
31
+ lambda c: f'a cropped photo of a {c}.',
32
+ lambda c: f'a plastic {c}.',
33
+ lambda c: f'a photo of the dirty {c}.',
34
+ lambda c: f'a jpeg corrupted photo of a {c}.',
35
+ lambda c: f'a blurry photo of the {c}.',
36
+ lambda c: f'a photo of the {c}.',
37
+ lambda c: f'a good photo of the {c}.',
38
+ lambda c: f'a rendering of the {c}.',
39
+ lambda c: f'a {c} in a video game.',
40
+ lambda c: f'a photo of one {c}.',
41
+ lambda c: f'a doodle of a {c}.',
42
+ lambda c: f'a close-up photo of the {c}.',
43
+ lambda c: f'a photo of a {c}.',
44
+ lambda c: f'the origami {c}.',
45
+ lambda c: f'the {c} in a video game.',
46
+ lambda c: f'a sketch of a {c}.',
47
+ lambda c: f'a doodle of the {c}.',
48
+ lambda c: f'a origami {c}.',
49
+ lambda c: f'a low resolution photo of a {c}.',
50
+ lambda c: f'the toy {c}.',
51
+ lambda c: f'a rendition of the {c}.',
52
+ lambda c: f'a photo of the clean {c}.',
53
+ lambda c: f'a photo of a large {c}.',
54
+ lambda c: f'a rendition of a {c}.',
55
+ lambda c: f'a photo of a nice {c}.',
56
+ lambda c: f'a photo of a weird {c}.',
57
+ lambda c: f'a blurry photo of a {c}.',
58
+ lambda c: f'a cartoon {c}.',
59
+ lambda c: f'art of a {c}.',
60
+ lambda c: f'a sketch of the {c}.',
61
+ lambda c: f'a embroidered {c}.',
62
+ lambda c: f'a pixelated photo of a {c}.',
63
+ lambda c: f'itap of the {c}.',
64
+ lambda c: f'a jpeg corrupted photo of the {c}.',
65
+ lambda c: f'a good photo of a {c}.',
66
+ lambda c: f'a plushie {c}.',
67
+ lambda c: f'a photo of the nice {c}.',
68
+ lambda c: f'a photo of the small {c}.',
69
+ lambda c: f'a photo of the weird {c}.',
70
+ lambda c: f'the cartoon {c}.',
71
+ lambda c: f'art of the {c}.',
72
+ lambda c: f'a drawing of the {c}.',
73
+ lambda c: f'a photo of the large {c}.',
74
+ lambda c: f'a black and white photo of a {c}.',
75
+ lambda c: f'the plushie {c}.',
76
+ lambda c: f'a dark photo of a {c}.',
77
+ lambda c: f'itap of a {c}.',
78
+ lambda c: f'graffiti of the {c}.',
79
+ lambda c: f'a toy {c}.',
80
+ lambda c: f'itap of my {c}.',
81
+ lambda c: f'a photo of a cool {c}.',
82
+ lambda c: f'a photo of a small {c}.',
83
+ lambda c: f'a tattoo of the {c}.',
84
+ ]
radseg/radseg.py ADDED
@@ -0,0 +1,547 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Includes the RADSeg Encoder.
2
+
3
+ The module only relies on the base.py and prompt_templates.py files in
4
+ image_encoders. Encoder can be copied with those files to your own project.
5
+
6
+ Typical Usage:
7
+
8
+ rgb_img = torchvision.io.read_image(rgb_path)
9
+ rgb_img = rgb_img.float() / 255
10
+ rgb_img = torch.nn.functional.interpolate(
11
+ rgb_img.unsqueeze(0),size=(512, 512))
12
+
13
+ labels = ["car", "person"]
14
+
15
+ enc = RADSegEncoder(model_version="c-radio_v3-b", lang_model="siglip2")
16
+
17
+ feat_map = enc.encode_image_to_feat_map(rgb_img)
18
+ lang_aligned_feat_map = enc.align_spatial_features_with_language(feat_map)
19
+
20
+ text_features = enc.encode_labels(labels)
21
+
22
+ from rayfronts.utils import compute_cos_sim
23
+ r = compute_cos_sim(text_features, lang_aligned_feat_map, softmax=True)
24
+ """
25
+
26
+ from typing_extensions import override, List, Tuple
27
+
28
+ import torch
29
+ import numpy as np
30
+
31
+ from radseg.base import ImageSemSegEncoder
32
+ from segment_anything import sam_model_registry, SamPredictor
33
+ from radseg.sam_utils import sam_refinement
34
+
35
+ import torch
36
+ import torch.nn as nn
37
+ from torch.nn import functional as F
38
+ from timm.layers import use_fused_attn
39
+ import math
40
+
41
+ def compute_cos_sim(vec1: torch.FloatTensor,
42
+ vec2: torch.FloatTensor,
43
+ softmax: bool = False) -> torch.FloatTensor:
44
+ """Compute cosine similarity between two batches of D dim vectors.
45
+
46
+ Args:
47
+ vec1: NxC float tensor representing batch of vectors
48
+ vec2: MxC float tensor representing batch of vectors
49
+ softmax: If False, cosine similarity is returned. If True, softmaxed
50
+ probability is returned across the N dimension.
51
+ Returns:
52
+ result: MxN float tensor representing similarity/prob. where result[0,1]
53
+ represents the similarity of vec1[0] with vec2[1]
54
+ """
55
+ N, C1 = vec1.shape
56
+ M, C2 = vec2.shape
57
+ if C1 != C2:
58
+ raise ValueError(f"vec1 feature dimension '{C1}' does not match vec2"
59
+ f"feature dimension '{C2}'")
60
+ C = C1
61
+
62
+ vec1 = vec1 / vec1.norm(dim = -1, keepdim = True)
63
+ vec1 = vec1.reshape(1, N, 1, C)
64
+
65
+ vec2 = vec2 / vec2.norm(dim=-1, keepdim = True)
66
+ vec2 = vec2.reshape(M, 1, C, 1)
67
+
68
+ sim = (vec1 @ vec2).reshape(M, N)
69
+ if softmax:
70
+ return torch.softmax(100 * sim, dim = -1)
71
+ else:
72
+ return sim
73
+
74
+ class SelfCorrelatingRecursiveAttn(nn.Module):
75
+ def __init__(
76
+ self,
77
+ orig_attn,
78
+ device,
79
+ dim: int,
80
+ qk_norm: bool = False,
81
+ scra_scaling: int = 10
82
+ ) -> None:
83
+ super().__init__()
84
+ num_heads = orig_attn.num_heads
85
+ assert dim % num_heads == 0, "dim should be divisible by num_heads"
86
+ self.num_heads = num_heads
87
+ self.head_dim = dim // num_heads
88
+ self.scale = self.head_dim ** -0.5
89
+ self.fused_attn = use_fused_attn()
90
+
91
+ self.qkv = orig_attn.qkv
92
+ self.q_norm = orig_attn.q_norm if qk_norm else nn.Identity()
93
+ self.k_norm = orig_attn.k_norm if qk_norm else nn.Identity()
94
+ self.attn_drop = orig_attn.attn_drop
95
+ self.proj = orig_attn.proj
96
+ self.proj_drop = orig_attn.proj_drop
97
+ self.device = device
98
+ self.scra_scaling = scra_scaling
99
+
100
+ def forward(self, x: torch.Tensor, *args, **kwargs) -> torch.Tensor:
101
+ B, N, C = x.shape
102
+ x_out = self.custom_attn(x.permute(1, 0, 2))
103
+ x_out = x_out.permute(1, 0, 2)
104
+ return x_out
105
+
106
+
107
+ def custom_attn(self, x):
108
+ num_heads = self.num_heads
109
+ num_tokens, bsz, embed_dim = x.size()
110
+ head_dim = embed_dim // num_heads
111
+ scale = head_dim ** -0.5
112
+ q, k, v = self.qkv(x).chunk(3, dim=-1)
113
+ q, k = self.q_norm(q), self.k_norm(k)
114
+
115
+ q = q.contiguous().view(-1, bsz * num_heads, head_dim).transpose(0, 1)
116
+ k = k.contiguous().view(-1, bsz * num_heads, head_dim).transpose(0, 1)
117
+ v = v.contiguous().view(-1, bsz * num_heads, head_dim).transpose(0, 1)
118
+
119
+ attn_weights = torch.bmm(q, k.transpose(1, 2)) * scale
120
+ attn_weights = F.softmax(attn_weights, dim=-1)
121
+ attn_output = torch.bmm(attn_weights, v)
122
+ attn_output = attn_output.transpose(0, 1).contiguous().view(
123
+ -1, bsz, embed_dim)
124
+ attn_output = self.proj(attn_output)
125
+ attn_output = self.proj_drop(attn_output)
126
+
127
+ # Self Correlating Recursive Attention
128
+ attn_output = attn_output.view(-1, bsz * num_heads, head_dim).transpose(0, 1)
129
+ sim_tokens = F.normalize(attn_output,dim = -1)
130
+ sim_matrix = torch.bmm(sim_tokens,sim_tokens.transpose(1,2))*self.scra_scaling
131
+ sim_matrix[sim_matrix < 0] = -torch.inf
132
+ sim_matrix = F.softmax(sim_matrix,dim=-1)
133
+ attn_output = torch.bmm(sim_matrix, v)
134
+ attn_output = attn_output.transpose(0, 1).contiguous().view(
135
+ -1, bsz, embed_dim)
136
+ attn_output = self.proj(attn_output)
137
+ attn_output = self.proj_drop(attn_output)
138
+
139
+ return attn_output
140
+
141
+ class RADSegEncoder(ImageSemSegEncoder):
142
+ # TODO: Split into feature encoder and semseg encoder for clarity instead
143
+ # of switching behaviors based on flags.
144
+
145
+ def __init__(self,
146
+ device: str = None,
147
+ model_version: str = "radio_v3-b",
148
+ lang_model: str ="siglip2",
149
+ return_radio_features: bool = True,
150
+ compile: bool = False,
151
+ amp: bool = False,
152
+ predict: bool = False,
153
+ classes: List[str] = None,
154
+ text_query_mode: str = "labels",
155
+ prediction_thresh: float = 0.0,
156
+ prompt_denoising_thresh: float = 0.5,
157
+ scra_scaling: float = 10.0,
158
+ scga_scaling: float = 10.0,
159
+ slide_crop: int = 336,
160
+ slide_stride: int = 224,
161
+ # Sam refinement args
162
+ sam_refinement: bool = False,
163
+ sam_ckpt: str = 'sam_vit_h_4b8939.pth',
164
+ coarse_thresh: float = 0.10,
165
+ minimal_area: int = 225,
166
+ sam_mask_coff: float = 0.005,
167
+ sam_iou_thresh: float = 0.9):
168
+
169
+ super().__init__(device)
170
+
171
+ self.compile = compile
172
+ self.amp = amp
173
+ self.model_version = model_version
174
+ self.return_radio_features = return_radio_features
175
+ adaptor_names = [lang_model, "sam"]
176
+ self.model = torch.hub.load("NVlabs/RADIO", "radio_model",
177
+ version=model_version, progress=True,
178
+ skip_validation=True,
179
+ adaptor_names=adaptor_names)
180
+ self.model.eval()
181
+ self.model = self.model.to(self.device)
182
+ # Steal adaptors from RADIO so it does not auto compute adaptor output.
183
+ # We want to control when that happens.
184
+ self.lang_adaptor = self.model.adaptors[lang_model]
185
+ self.sam_adaptor = self.model.adaptors["sam"]
186
+ self.model.adaptors = None
187
+ last_block = self.model.model.blocks[-1]
188
+ last_block.attn = SelfCorrelatingRecursiveAttn(
189
+ last_block.attn,
190
+ dim=self.model.model.embed_dim,
191
+ device=self.device,
192
+ scra_scaling=scra_scaling)
193
+
194
+ if self.compile:
195
+ self.model.compile(fullgraph=True, options={"triton.cudagraphs":True})
196
+ self.lang_adaptor.compile(fullgraph=True, options={"triton.cudagraphs":True})
197
+
198
+ self.predict = predict
199
+ self.prediction_thresh = prediction_thresh
200
+ self.text_query_mode = text_query_mode
201
+ self.prompt_denoising_thresh = prompt_denoising_thresh
202
+
203
+ if self.predict:
204
+ if classes is None or len(classes) < 1:
205
+ raise Exception("Must pass list of classes when predict is True")
206
+ self.prompts = list(classes)
207
+
208
+ if len(self.prompts[0]) == 0: # Remove ignore class so we don't prompt
209
+ self.prompts.pop(0)
210
+ self._cat_index_to_name = {0: ''}
211
+ self._cat_index_to_name.update({i+1: v for i,v in enumerate(self.prompts)})
212
+ self._cat_name_to_index = {
213
+ v: k for k, v in self._cat_index_to_name.items()
214
+ }
215
+ if self.text_query_mode == "labels":
216
+ self.text_embeds = self.encode_labels(self.prompts, onehot=False)
217
+ elif self.text_query_mode == "prompts":
218
+ self.text_embeds = self.encode_prompts(self.prompts, onehot=False)
219
+ else:
220
+ raise ValueError("Invalid query type")
221
+
222
+
223
+ self.slide_stride = slide_stride
224
+ self.slide_crop = slide_crop
225
+ self.scga_scaling = scga_scaling
226
+
227
+ # Sam refinement args
228
+ self.sam_refinement = sam_refinement
229
+ if sam_refinement:
230
+ self.sam_iou_thresh = sam_iou_thresh
231
+ self.coarse_thresh = coarse_thresh
232
+ self.minimal_area = minimal_area
233
+ self.sam_mask_coff = sam_mask_coff
234
+ self.sam = sam_model_registry["vit_h"](checkpoint=sam_ckpt).to(device=self.device).eval()
235
+ self.sam_predictor = SamPredictor(self.sam)
236
+ del self.sam.image_encoder.blocks
237
+ del self.sam.image_encoder.patch_embed
238
+ torch.cuda.empty_cache()
239
+
240
+ @property
241
+ @override
242
+ def num_classes(self) -> int:
243
+ return len(self.prompts) + 1
244
+
245
+ @property
246
+ @override
247
+ def cat_index_to_name(self):
248
+ return self._cat_index_to_name
249
+
250
+ @property
251
+ @override
252
+ def cat_name_to_index(self):
253
+ return self._cat_name_to_index
254
+
255
+ @override
256
+ def encode_labels(self, labels: List[str], onehot: bool = True) -> torch.FloatTensor:
257
+ if self.predict and onehot:
258
+ return super().encode_labels(labels)
259
+ prompts_per_label = self.insert_labels_into_templates(labels)
260
+ all_text_features = list()
261
+ for i in range(len(labels)):
262
+ text_features = self.encode_prompts(prompts_per_label[i], onehot=False)
263
+ text_features = text_features.mean(dim=0, keepdim=True)
264
+ all_text_features.append(text_features)
265
+
266
+ all_text_features = torch.cat(all_text_features, dim=0)
267
+ return all_text_features
268
+
269
+ @override
270
+ def encode_prompts(self, prompts: List[str], onehot: bool = True) -> torch.FloatTensor:
271
+ if self.predict and onehot:
272
+ return super().encode_labels(prompts)
273
+ with torch.autocast("cuda", dtype=torch.float16, enabled=self.amp):
274
+ text = self.lang_adaptor.tokenizer(prompts).to(self.device)
275
+ text_features = self.lang_adaptor.encode_text(text)
276
+ text_features /= text_features.norm(dim=-1, keepdim=True)
277
+ return text_features
278
+
279
+ @override
280
+ def encode_image_to_feat_map(
281
+ self, rgb_image: torch.FloatTensor, orig_img_size: Tuple[int] = None,
282
+ return_preds: bool = False, ignore_label: bool = True) -> torch.FloatTensor:
283
+ if orig_img_size is None:
284
+ orig_img_size = rgb_image.shape[-2:]
285
+
286
+ if self.slide_crop > 0:
287
+ feat_map = self._sliding_inference(rgb_image, stride=self.slide_stride, crop_size=self.slide_crop)
288
+ else:
289
+ feat_map = self._single_inference(rgb_image)
290
+
291
+ feat_map = self._self_correlating_global_aggregation(feat_map)
292
+ if not self.predict:
293
+ return feat_map
294
+
295
+ # Predict
296
+ seg_probs = self._get_seg_logits(feat_map)
297
+ seg_probs = nn.functional.interpolate(
298
+ seg_probs, size=orig_img_size, mode='bilinear', align_corners=False)
299
+
300
+ B, C, H, W = seg_probs.shape
301
+ num_cls = C
302
+
303
+ # Prompt denoising
304
+ seg_probs = seg_probs.permute(0, 2, 3, 1).reshape(-1, C)
305
+ max_sim_per_class = torch.max(seg_probs, dim=0).values
306
+
307
+ low_conf_classes = torch.argwhere(max_sim_per_class < self.prompt_denoising_thresh)
308
+ seg_probs[:, low_conf_classes] = 0
309
+
310
+ seg_probs = seg_probs.reshape(B, H, W, C).permute(0, 3, 1, 2) # BxCxHxW
311
+ max_sim_per_pixel, seg_pred = torch.max(seg_probs, dim=1, keepdim=True) # Bx1xHxW
312
+
313
+ if self.sam_refinement:
314
+ seg_pred_ref = list()
315
+ seg_probs_ref = list()
316
+ for b in range(B):
317
+ sam_image, new_h, new_w = self._preprocess_sam(rgb_image[b], target_size=1024)
318
+ image_features = self._single_inference(sam_image)
319
+ sam_features = self._get_sam_spatial_features(image_features).float()
320
+ sam_features = self._interpolate_to_sam_dims(sam_features)
321
+ sam_features = self.sam_predictor.model.image_encoder.neck(sam_features)
322
+ self.sam_predictor.features = sam_features
323
+ self.sam_predictor.is_image_set = True
324
+ self.sam_predictor.original_size = orig_img_size
325
+ self.sam_predictor.input_size = (new_h, new_w)
326
+
327
+ refined_masks, scores, refined_logits, prompt_boxes = sam_refinement(
328
+ orig_img_size, seg_pred[b], seg_probs[b], num_cls, self.sam_predictor,
329
+ self.coarse_thresh, self.minimal_area,
330
+ self.sam_mask_coff, self.sam_iou_thresh)
331
+
332
+ seg_pred_ref.append(refined_masks)
333
+ seg_probs_ref.append(refined_logits)
334
+ seg_pred = torch.stack(seg_pred_ref, dim=0)
335
+ seg_probs = torch.stack(seg_probs_ref, dim=0)
336
+
337
+ # Set low confidence predictions to the ignore label
338
+ if ignore_label:
339
+ seg_pred += 1
340
+ seg_pred[max_sim_per_pixel < self.prediction_thresh] = 0
341
+ seg_probs = torch.cat(
342
+ [torch.zeros_like(seg_probs[:, :1, :, :]), seg_probs], dim=1)
343
+ if return_preds:
344
+ return seg_probs, seg_pred
345
+ else:
346
+ return seg_probs
347
+
348
+ @override
349
+ def align_spatial_features_with_language(self, features: torch.FloatTensor,
350
+ onehot: bool = True):
351
+ if self.lang_adaptor is None:
352
+ raise ValueError("Cannot align to language without a lang model")
353
+ if not self.return_radio_features or (self.predict and onehot):
354
+ return features
355
+ B,C,H,W = features.shape
356
+ features = features.permute(0, 2, 3, 1).reshape(B, -1, C)
357
+ with torch.autocast("cuda", dtype=torch.float16, enabled=self.amp):
358
+ out = self.lang_adaptor.head_mlp(features)
359
+ return out.permute(0, 2, 1).reshape(B, -1, H, W)
360
+
361
+ def _get_sam_spatial_features(self, features: torch.FloatTensor):
362
+ if self.sam_adaptor is None:
363
+ raise ValueError("Cannot align to sam without a sam model")
364
+
365
+ B,C,H,W = features.shape
366
+ features = features.permute(0, 2, 3, 1).reshape(B, -1, C)
367
+
368
+ with torch.autocast("cuda", dtype=torch.float16, enabled=self.amp):
369
+ out = self.sam_adaptor.feat_mlp(features)
370
+
371
+ return out.permute(0, 2, 1).reshape(B, -1, H, W)
372
+
373
+ @override
374
+ def is_compatible_size(self, h: int, w: int):
375
+ hh, ww = self.get_nearest_size(h, w)
376
+ return hh == h and ww == w
377
+
378
+ @override
379
+ def get_nearest_size(self, h, w):
380
+ return self.model.get_nearest_supported_resolution(h, w)
381
+
382
+ def _interpolate_to_sam_dims(self, feature_map):
383
+ feat_h = feature_map.shape[2]
384
+ feat_w = feature_map.shape[3]
385
+ feature_map = F.interpolate(
386
+ feature_map, size=(feat_h, feat_w), mode='bilinear',
387
+ align_corners=False, antialias=True)
388
+ feature_map = F.pad(feature_map, (0, 64 - feat_w, 0, 64 - feat_h))
389
+ return feature_map
390
+
391
+ def _preprocess_sam(self, image: torch.Tensor, target_size: int = 1024) -> torch.Tensor:
392
+ """
393
+ Preprocess an image tensor for SAM.
394
+
395
+ Args:
396
+ image (torch.Tensor): Tensor of shape (C, H, W), values in [0, 255] or [0, 1].
397
+ target_size (int): Size to pad/resize to (default=1024).
398
+
399
+ Returns:
400
+ torch.Tensor: Preprocessed tensor of shape (1, 3, target_size, target_size).
401
+ """
402
+ if image.dim() != 3 or image.shape[0] != 3:
403
+ raise ValueError("Input must have shape (3, H, W)")
404
+
405
+ C, H, W = image.shape
406
+
407
+ if H >= W:
408
+ new_h = target_size
409
+ new_w = int(W * 64 /H) * 16
410
+ else:
411
+ new_w = target_size
412
+ new_h = int(H * 64 / W) * 16
413
+
414
+ image = image.unsqueeze(0) # add batch dim
415
+ image = F.interpolate(image, size=(new_h, new_w), mode="bilinear", align_corners=False)
416
+
417
+ return image, new_h, new_w
418
+
419
+ def _self_correlating_global_aggregation(self,feat_map):
420
+
421
+ b,tokens,h,w = feat_map.shape
422
+ feat_map = feat_map.flatten(2,3).transpose(1,2)
423
+ sim_tokens = F.normalize(feat_map,dim = -1)
424
+ sim_matrix = torch.bmm(sim_tokens,sim_tokens.transpose(1,2))
425
+ sim_matrix = (sim_matrix - torch.mean(sim_matrix)) * self.scga_scaling
426
+ sim_matrix[sim_matrix < 0] = -torch.inf
427
+ sim_matrix = F.softmax(sim_matrix,dim=-1)
428
+ attn_output = torch.bmm(sim_matrix, feat_map)
429
+ attn_output = attn_output.transpose(1,2).view(b,tokens,h,w)
430
+
431
+ return attn_output
432
+
433
+ def _get_seg_logits(self, feat_map):
434
+
435
+ image_features = self.align_spatial_features_with_language(feat_map, onehot=False)
436
+ feat_map = image_features.permute(0, 2, 3, 1)
437
+
438
+ B,H,W,C = feat_map.shape
439
+ feat_map = feat_map.reshape(-1, C)
440
+
441
+ cos_sim = compute_cos_sim(
442
+ self.text_embeds, feat_map,
443
+ softmax=True)
444
+
445
+ N = len(self.text_embeds)
446
+
447
+ cos_sim = cos_sim.reshape(B,H,W,N)
448
+ cos_sim = cos_sim.permute(0,3,1,2)
449
+
450
+ return cos_sim
451
+
452
+ def _preprocess_image(self, image, stride=16, slide_crop=336):
453
+ longer_side = max(image.shape[2:])
454
+ h, w = image.shape[2:]
455
+ if h%stride == 0 and w%stride == 0:
456
+ return image
457
+ if longer_side % stride != 0:
458
+ dst_longer = (longer_side // stride + 1) * stride
459
+ else:
460
+ dst_longer = longer_side
461
+ new_h = int(h * dst_longer / longer_side)
462
+ new_w = int(w * dst_longer / longer_side)
463
+ if new_h % stride != 0: new_h = (new_h // stride + 1) * stride
464
+ if new_w % stride != 0: new_w = (new_w // stride + 1) * stride
465
+ new_h, new_w = max(new_h, slide_crop), max(new_w, slide_crop)
466
+ image = torch.nn.functional.interpolate(image, (new_h, new_w), mode='bilinear', align_corners=False)
467
+
468
+ return image
469
+
470
+ def _get_windowed_imgs(self, img, patch_size=16):
471
+ stride, crop_size = self.slide_stride, self.slide_crop
472
+ if type(img) == list:
473
+ img = img[0].unsqueeze(0)
474
+ if type(stride) == int:
475
+ stride = (stride, stride)
476
+ if type(crop_size) == int:
477
+ crop_size = (crop_size, crop_size)
478
+
479
+ h_stride, w_stride = stride
480
+ h_crop, w_crop = crop_size
481
+ batch_size, _, h_img, w_img = img.shape
482
+ h_grids = max(h_img - h_crop + h_stride - 1, 0) // h_stride + 1
483
+ w_grids = max(w_img - w_crop + w_stride - 1, 0) // w_stride + 1
484
+ crop_imgs, patch_locs = [], []
485
+ for h_idx in range(h_grids):
486
+ for w_idx in range(w_grids):
487
+ y1 = h_idx * h_stride
488
+ x1 = w_idx * w_stride
489
+ y2 = min(y1 + h_crop, h_img)
490
+ x2 = min(x1 + w_crop, w_img)
491
+ y1 = max(y2 - h_crop, 0)
492
+ x1 = max(x2 - w_crop, 0)
493
+ crop_img = img[:, :, y1:y2, x1:x2]
494
+ assert y1 % patch_size == 0 and x1 % patch_size == 0
495
+ assert y2 % patch_size == 0 and x2 % patch_size == 0
496
+ patch_locs.append(torch.tensor([y1//patch_size, x1//patch_size, y2//patch_size, x2//patch_size]))
497
+ # pad image when (image_size % patch_size != 0)
498
+ crop_imgs.append(crop_img)
499
+
500
+ batched_imgs = torch.cat(crop_imgs, dim=0) # [n_patches, 3, h, w]
501
+ return batched_imgs, patch_locs, (h_grids,w_grids)
502
+
503
+ def _single_inference(self, rgb_image):
504
+ B, C, H, W = rgb_image.shape
505
+ H_, W_ = H // self.model.patch_size, W // self.model.patch_size
506
+ with torch.autocast("cuda", dtype=torch.float16, enabled=self.amp):
507
+ out = self.model(rgb_image).features
508
+ if not self.return_radio_features:
509
+ out = self.lang_adaptor.head_mlp(out)
510
+ return out.permute(0, 2, 1).reshape(B, -1, H_, W_)
511
+
512
+ def _sliding_inference(self, img, stride=224, crop_size=336):
513
+ """
514
+ Inference by sliding-window with overlap. If h_crop > h_img or w_crop > w_img,
515
+ the small patch will be used to decode without padding.
516
+ """
517
+
518
+ if type(img) == list:
519
+ img = img[0].unsqueeze(0)
520
+ if type(stride) == int:
521
+ stride = (stride, stride)
522
+ if type(crop_size) == int:
523
+ crop_size = (crop_size, crop_size)
524
+
525
+ img = self._preprocess_image(img, stride[0], crop_size[0])
526
+
527
+ batched_imgs, patch_locs, (h_grids,w_grids) = self._get_windowed_imgs(img, patch_size=16)
528
+ batch_size = img.shape[0]
529
+ _, _, h_img, w_img = img.shape
530
+
531
+ image_feats = self._single_inference(batched_imgs)
532
+ feat_dim = image_feats.shape[1]
533
+ dtype = image_feats.dtype
534
+ device = image_feats.device
535
+ h_feat = math.ceil(h_img / 16)
536
+ w_feat = math.ceil(w_img / 16)
537
+ feat_map = torch.zeros((batch_size, feat_dim, h_feat, w_feat),dtype = dtype, device=device)
538
+ count_mat = torch.zeros((batch_size, 1, h_feat, w_feat),dtype = dtype, device=device)
539
+ for h_idx in range(h_grids):
540
+ for w_idx in range(w_grids):
541
+ coord = patch_locs[h_idx*w_grids+w_idx]
542
+ img_feat = image_feats[h_idx*w_grids+w_idx]
543
+ feat_map[:, :,coord[0]:coord[2],coord[1]:coord[3]] += img_feat
544
+ count_mat[:, :,coord[0]:coord[2],coord[1]:coord[3]] += 1
545
+ feat_map = feat_map / count_mat # 1, D, dst_h, dst_w
546
+
547
+ return feat_map
radseg/sam_utils.py ADDED
@@ -0,0 +1,174 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import numpy as np
3
+ from skimage import measure
4
+ from torchvision.ops import masks_to_boxes
5
+ import torch.nn.functional as F
6
+
7
+
8
+ def split_connected_regions(segmentation: np.ndarray, seg_logits, split_last=False, minimal_area=0,
9
+ coarse_thresh=0.0,) -> dict:
10
+ """
11
+ Splits the segmentation mask into separate connected regions for each class.
12
+
13
+ Parameters:
14
+ segmentation (np.ndarray): Semantic segmentation mask of shape [C, H, W].
15
+ seg_logits (np.ndarray): Segmentation logits of shape [C, H, W].
16
+ split_last (bool): If True, the last class is not split into connected regions and regarded as the bg.
17
+ minimal_area (int): Minimal area of a connected region to be considered.
18
+ Returns:
19
+ dict: A dictionary where keys are class indices and values are lists of masks.
20
+ """
21
+ C, H, W = segmentation.shape
22
+ regions, boxes, clip_box_score, points = {}, {}, {}, {}
23
+ for c in range(C):
24
+ if not split_last and c == C - 1: continue
25
+ regions[c], points[c], clip_box_score[c],boxes[c] = [], [], [], []
26
+ class_mask = segmentation[c]
27
+ class_logit = seg_logits[c]
28
+ if class_logit.max() < coarse_thresh: continue
29
+ # Label connected regions
30
+ labeled_mask = measure.label(class_mask, connectivity=2) # 8-connectivity
31
+ num_regions = labeled_mask.max()
32
+
33
+ for region_label in range(1, num_regions + 1):
34
+ region_mask = (labeled_mask == region_label).astype(np.uint8)
35
+ if region_mask.sum() < minimal_area: continue
36
+ # Calculate the mean logit score of the region
37
+ region_score = class_logit[region_mask.astype(bool)].mean()
38
+ #find the max logit position of the class_logit(H,W) and add it to points[c]
39
+ region_logit = class_logit * torch.tensor(region_mask).to(class_logit.device)
40
+ max_pos = torch.argmax(region_logit)
41
+ max_h, max_w = max_pos // class_logit.size(1), max_pos % class_logit.size(1)
42
+ points[c].append(torch.tensor([max_w, max_h]))
43
+ regions[c].append(region_mask)
44
+ clip_box_score[c].append(region_score)
45
+
46
+ if len(regions[c]):
47
+ regions[c] = torch.from_numpy(np.stack(regions[c]))
48
+ if regions[c] == []: boxes[c] = regions[c]
49
+ else: boxes[c] = masks_to_boxes(regions[c])
50
+ return regions, boxes, clip_box_score, points
51
+
52
+ def map_refinement_coarse(refined_masks, refined_logits, coarse_boxes, seg_logits, img_shape):
53
+ refined_whole_logits = torch.zeros([len(coarse_boxes),img_shape[0],img_shape[1]],device=refined_masks.device) # C, H, W
54
+ cumsum = 0
55
+ for i in range(len(coarse_boxes)):
56
+ if coarse_boxes[i] == []: continue
57
+ tmp_cls_num = coarse_boxes[i].shape[0]
58
+ tmp_cls_mask = refined_masks[cumsum:cumsum+tmp_cls_num,0].sum(dim=0).bool() #H, W
59
+ tmp_cls_logit = (refined_logits[cumsum:cumsum+tmp_cls_num,0] * seg_logits[i]).max(dim=0) #H, W
60
+ refined_whole_logits[i] = tmp_cls_logit.values * tmp_cls_mask
61
+ cumsum += tmp_cls_num
62
+
63
+ refined_whole_masks = refined_whole_logits.argmax(0, keepdim=True)
64
+ return refined_whole_masks, refined_whole_logits
65
+
66
+ def map_failed_regions(refined_masks, refined_logits, failed_regions, seg_logits, segmentation):
67
+ failed_area = failed_regions.sum(dim=0, keepdim=True)!=0 #1, H, W
68
+ failed_logit = failed_area * seg_logits #C, H, W
69
+ refined_logits = torch.where(failed_logit>refined_logits, failed_logit, refined_logits)
70
+ refined_masks = torch.where(failed_logit.max(dim=0, keepdim=True)[0]>refined_logits.max(dim=0, keepdim=True)[0], segmentation, refined_masks)
71
+ return refined_masks, refined_logits
72
+
73
+ def sam_refinement(img_size, segmentations, seg_logits, num_classes, predictor, coarse_thresh=0.0, minimal_area=0,
74
+ sam_mask_coff=0.005,sam_iou_thresh=0.9):
75
+ '''
76
+ Args:
77
+ img: str or np.ndarray
78
+ segmentations: torch.Tensor, shape=[C, H, W]
79
+ seg_logits: torch.Tensor, shape=[C, H, W]
80
+ num_classes: int
81
+ predictor: Predictor
82
+ coarse_thresh: float
83
+ minimal_area: int
84
+ sam_mask_coff: float
85
+ sam_iou_thresh: float
86
+ '''
87
+ #downsample the segmentation and seg_logits to accelerate split_connected_regions
88
+ down_ratio = 2
89
+ dh, dw = seg_logits.shape[-2] // down_ratio, seg_logits.shape[-1] // down_ratio
90
+ if down_ratio != 1:
91
+ segmentations_down = F.interpolate(segmentations.unsqueeze(0).half(), [dh, dw], mode='nearest').squeeze(0).long()
92
+ seg_logits_down = F.interpolate(seg_logits.unsqueeze(0), [dh, dw], mode='bilinear', align_corners=False).squeeze(0)
93
+ else:
94
+ segmentations_down = segmentations
95
+ seg_logits_down = seg_logits
96
+
97
+ if coarse_thresh > 0:
98
+ segmentations_down[seg_logits_down.max(0, keepdim=True)[0] < coarse_thresh] = num_classes
99
+ cls_pred = F.one_hot(segmentations_down.squeeze(0).long(), num_classes=num_classes+1).permute(2, 0, 1).float() # [C, H, W]
100
+ split_last = False
101
+ else:
102
+ cls_pred = F.one_hot(segmentations_down.squeeze(0).long(), num_classes=num_classes).permute(2, 0, 1).float()
103
+ split_last = True
104
+
105
+ coarse_regions, coarse_boxes, coarse_boxes_score, coarse_points = split_connected_regions(cls_pred.cpu().numpy(), seg_logits_down,
106
+ split_last=split_last, minimal_area=minimal_area,coarse_thresh=coarse_thresh)
107
+ combined_boxes,combined_points = [], []
108
+ cls_wise_masks, cls_wise_scores, cls_wise_logits = [], [], []
109
+ failed_regions = []
110
+ predictor.features = predictor.features.float()
111
+ for i in range(num_classes):
112
+ tmp_r = coarse_regions.get(i, [])
113
+ tmp_b = coarse_boxes.get(i, [])
114
+ tmp_p = coarse_points.get(i, [])
115
+
116
+ if tmp_r == []: continue
117
+ cls_wise_r = seg_logits_down[i].unsqueeze(0)
118
+ #resize to longer side = 256
119
+ new_h, new_w = predictor.transform.get_preprocess_shape(img_size[0], img_size[1], 256)
120
+ cls_wise_r = F.interpolate(cls_wise_r.unsqueeze(0), [new_h, new_w], mode="bilinear", align_corners=False).squeeze(0)
121
+ #pad to 256x256
122
+ cls_wise_r = F.pad(cls_wise_r, (0, 256-new_w, 0, 256-new_h), mode='constant', value=0)
123
+ cls_wise_r = torch.where(cls_wise_r > coarse_thresh, sam_mask_coff*cls_wise_r, torch.tensor(0.0).type_as(segmentations)).float()
124
+ cls_wise_b = predictor.transform.apply_boxes_torch(tmp_b.type_as(segmentations), [dh, dw]).float()
125
+ cls_wise_p = torch.stack(tmp_p).type_as(segmentations)
126
+ cls_wise_p = predictor.transform.apply_coords_torch(cls_wise_p, [dh, dw]).unsqueeze(1)
127
+ cls_wise_mask, cls_wise_score, cls_wise_logit = predictor.predict_torch(
128
+ point_coords=cls_wise_p.float(), # input_point,
129
+ point_labels=torch.ones([cls_wise_p.shape[0], 1]).type_as(segmentations).float(), # input_label,
130
+ boxes=cls_wise_b,
131
+ mask_input=cls_wise_r,
132
+ multimask_output=False,
133
+ )
134
+
135
+ valid_masks = (cls_wise_score > sam_iou_thresh).squeeze(-1)
136
+ if valid_masks.sum() != tmp_r.shape[0]:
137
+ failed_regions.append(tmp_r[~valid_masks.cpu()])
138
+ if valid_masks.sum() == 0:
139
+ coarse_boxes[i] = []
140
+ continue
141
+ cls_wise_masks.append(cls_wise_mask[valid_masks])
142
+ cls_wise_scores.append(cls_wise_score[valid_masks])
143
+ cls_wise_logits.append(cls_wise_logit[valid_masks])
144
+
145
+ tmp_p = torch.stack(tmp_p)
146
+ tmp_b = tmp_b[valid_masks.cpu()]
147
+ tmp_p = tmp_p[valid_masks.cpu()]
148
+ coarse_boxes[i] = coarse_boxes[i][valid_masks.cpu()]
149
+
150
+ combined_boxes.append(tmp_b)
151
+ combined_points.append(tmp_p)
152
+
153
+ if len(combined_boxes) == 0:
154
+ return segmentations, None, seg_logits, combined_boxes
155
+
156
+ combined_boxes = torch.cat(combined_boxes, dim=0).to(predictor.device)
157
+ masks, scores, logits = torch.cat(cls_wise_masks, dim=0), torch.cat(cls_wise_scores, dim=0), torch.cat(cls_wise_logits, dim=0)
158
+ logits = predictor.model.postprocess_masks(logits, predictor.input_size, predictor.original_size)
159
+
160
+ combined_boxes = combined_boxes * down_ratio
161
+ if logits.shape[-1] != img_size[1] or logits.shape[-2] != img_size[0]:
162
+ logits = F.interpolate(logits, img_size[:2], mode='bilinear', align_corners=False)
163
+ masks = logits>predictor.model.mask_threshold
164
+
165
+ refined_masks, refined_logits = map_refinement_coarse(masks, logits.sigmoid(), coarse_boxes, seg_logits, img_size,)
166
+
167
+ refined_masks = torch.where(refined_logits.sum(dim=0,keepdim=True) == 0, segmentations, refined_masks)
168
+ if len(failed_regions):
169
+ failed_regions = torch.cat(failed_regions, dim=0).to(predictor.device)
170
+ if down_ratio != 1:
171
+ failed_regions = F.interpolate(failed_regions.unsqueeze(0).float(), img_size[:2], mode='nearest').squeeze(0).type_as(segmentations)
172
+ refined_masks, refined_logits = map_failed_regions(refined_masks, refined_logits, failed_regions, seg_logits, segmentations)
173
+
174
+ return refined_masks, scores, refined_logits,combined_boxes
radseg_demo.py ADDED
@@ -0,0 +1,285 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ import os
3
+ import logging
4
+ import base64
5
+ from io import BytesIO
6
+ import colorsys
7
+ from functools import partial
8
+
9
+ import gradio as gr
10
+ import spaces
11
+ from PIL import Image
12
+ import numpy as np
13
+ import torch
14
+ import torch.nn.functional as F
15
+ from matplotlib import cm
16
+
17
+ # Add current directory to sys.path to find radseg module
18
+ sys.path.insert(0, os.path.abspath(os.path.dirname(__file__)))
19
+ from radseg.radseg import RADSegEncoder
20
+
21
+ logger = logging.getLogger(__name__)
22
+ logging.basicConfig(level=logging.INFO)
23
+
24
+ # Global state to keep track of prompts and colors (similar to inspiration app)
25
+ prompt_list = []
26
+ color_list = []
27
+
28
+ def apply_colormap(image: np.ndarray, cmap_name='viridis') -> np.ndarray:
29
+ """Apply a colormap to a grayscale image and return an RGB uint8 image."""
30
+ if image.dtype != np.float16 and image.dtype != np.float32 and image.dtype != np.float64:
31
+ image = image.astype(np.float32) / 255.0
32
+ image = np.clip(image, 0, 1)
33
+ cmap = cm.get_cmap(cmap_name)
34
+ colored = cmap(image)[:, :, :3] # Drop alpha channel
35
+ return (colored * 255).astype(np.uint8)
36
+
37
+ def numpy_to_base64(img_array):
38
+ """Convert a NumPy array image to base64 string."""
39
+ if img_array.dtype != np.uint8:
40
+ img_array = (img_array * 255).astype(np.uint8)
41
+ pil_img = Image.fromarray(img_array)
42
+ buffered = BytesIO()
43
+ pil_img.save(buffered, format="PNG")
44
+ return base64.b64encode(buffered.getvalue()).decode()
45
+
46
+ def make_grid_output(images, labels):
47
+ html = """
48
+ <div style='display: grid; grid-template-columns: repeat(3, 1fr); gap: 15px;'>
49
+ """
50
+ for img_array, label in zip(images, labels):
51
+ img_str = numpy_to_base64(img_array)
52
+ html += f"""
53
+ <div style='text-align: center;'>
54
+ <div style='font-weight: bold; margin-bottom: 5px;'>{label}</div>
55
+ <img src='data:image/png;base64,{img_str}' style='width: 100%; height: auto; border: 1px solid #ccc;' />
56
+ </div>
57
+ """
58
+ html += "</div>"
59
+ return html
60
+
61
+ def generate_distinct_color(index):
62
+ """Generate visually distinct colors using HSV color space."""
63
+ hue = (index * 0.61803398875) % 1 # golden ratio
64
+ r, g, b = colorsys.hsv_to_rgb(hue, 0.5, 0.95)
65
+ return '#%02x%02x%02x' % (int(r*255), int(g*255), int(b*255))
66
+
67
+ def add_prompt(prompts):
68
+ for prompt in prompts.split("\n"):
69
+ prompt = prompt.strip()
70
+ if not prompt or prompt in prompt_list:
71
+ continue
72
+ color = generate_distinct_color(len(prompt_list))
73
+ prompt_list.append(prompt)
74
+ color_list.append(color)
75
+
76
+ colored_prompts = [
77
+ f"<span style='background-color:{color}; color:#FFFFFF; padding: 2px 5px; border-radius: 3px; margin: 2px; display: inline-block;'>{p}</span>"
78
+ for p, color in zip(prompt_list, color_list)
79
+ ]
80
+ return gr.update(value=""), gr.update(value=" ".join(colored_prompts))
81
+
82
+ def clear_prompts():
83
+ prompt_list.clear()
84
+ color_list.clear()
85
+ return gr.update(value=""), gr.update(value="")
86
+
87
+ def on_page_load():
88
+ prompt_list.clear()
89
+ color_list.clear()
90
+ return gr.update(value="")
91
+
92
+ use_templates = True
93
+ model_version = "c-radio_v3-b"
94
+ lang_model = "siglip2"
95
+
96
+ # Load model lazily
97
+ encoder = None
98
+ def get_encoder(scra_scaling, scga_scaling, slide_crop, slide_stride):
99
+ global encoder
100
+ if encoder is None:
101
+ logger.info(f"Loading encoder: {model_version} with {lang_model}")
102
+ try:
103
+ # We don't pass classes yet as we will handle that in inference
104
+ # or by manually encoding prompts.
105
+ # To avoid the "Must pass list of classes when predict is True" error,
106
+ # we'll use predict=False for the base encoder and manage predictions ourselves.
107
+ encoder = RADSegEncoder(
108
+ model_version=model_version,
109
+ lang_model=lang_model,
110
+ scra_scaling=scra_scaling,
111
+ scga_scaling=scga_scaling,
112
+ slide_crop=slide_crop,
113
+ slide_stride=slide_stride,
114
+ sam_refinement=False,
115
+ predict=False,
116
+ device="cuda"
117
+ )
118
+ except Exception as e:
119
+ logger.error(f"Error loading encoder: {e}")
120
+ raise gr.Error(f"Failed to load encoder: {e}")
121
+ else:
122
+ encoder.slide_stride = slide_stride
123
+ encoder.slide_crop = slide_crop
124
+ encoder.scga_scaling = scga_scaling
125
+ encoder.model.model.blocks[-1].attn.scra_scaling = scra_scaling
126
+
127
+ return encoder
128
+
129
+ @spaces.GPU
130
+ @torch.inference_mode()
131
+ def process_all(input_image, scra_scaling, scga_scaling, use_sliding_window, window_size, window_stride, softmax, resolution):
132
+ global encoder
133
+ N = len(prompt_list)
134
+ if N == 0:
135
+ raise gr.Error("You must add some prompts", duration=5)
136
+
137
+ yield "Initializing encoder..."
138
+ device = "cuda" if torch.cuda.is_available() else "cpu"
139
+ slide_crop = window_size if use_sliding_window else 0
140
+ slide_stride = window_stride if use_sliding_window else 224
141
+ encoder = get_encoder(scra_scaling, scga_scaling, slide_crop, slide_stride)
142
+
143
+ yield "Processing image..."
144
+ # Convert numpy to torch
145
+ orig_h, orig_w = input_image.shape[:2]
146
+
147
+ # Resize to requested resolution (maintaining aspect ratio)
148
+ if max(orig_h, orig_w) > resolution:
149
+ scale = resolution / max(orig_h, orig_w)
150
+ new_h, new_w = int(orig_h * scale), int(orig_w * scale)
151
+ # Ensure divisible by 16 for VIT/RADIO if needed, though slide crop might handle it
152
+ new_h = (new_h // 16) * 16
153
+ new_w = (new_w // 16) * 16
154
+ input_image_resized = np.array(Image.fromarray(input_image).resize((new_w, new_h), resample=Image.BILINEAR))
155
+ else:
156
+ input_image_resized = input_image
157
+
158
+ image_size = input_image_resized.shape[:2]
159
+ tensor_image = torch.from_numpy(input_image_resized).permute(2, 0, 1).to(device).float() / 255.0
160
+
161
+ # Run RADSeg processing
162
+ # Note: RADSegEncoder handle sliding window if slide_crop is set.
163
+ # We can temporarily override resolution if needed but let's use the encoder's defaults.
164
+
165
+ # Manual similarity calculation to match inspiration app's heatmap per prompt
166
+ yield "Computing features..."
167
+ feat_map = encoder.encode_image_to_feat_map(tensor_image.unsqueeze(0), orig_img_size=image_size)
168
+
169
+ yield "Aligning features..."
170
+ aligned_feats = encoder.align_spatial_features_with_language(feat_map, onehot=False)
171
+
172
+ yield "Encoding prompts..."
173
+ if use_templates:
174
+ prompt_embeds = encoder.encode_labels(prompt_list)
175
+ else:
176
+ prompt_embeds = encoder.encode_prompts(prompt_list)
177
+
178
+ yield "Computing similarity..."
179
+ B, C, H, W = aligned_feats.shape
180
+ aligned_feats_flat = aligned_feats.permute(0, 2, 3, 1).reshape(-1, C)
181
+
182
+ # Compute cosine similarity
183
+ # similarity: [N_prompts, H*W]
184
+ vec1 = prompt_embeds / prompt_embeds.norm(dim=-1, keepdim=True)
185
+ vec2 = aligned_feats_flat / aligned_feats_flat.norm(dim=-1, keepdim=True)
186
+ sim = (vec1 @ vec2.t()) # [N, H*W]
187
+
188
+ if softmax:
189
+ sim = torch.softmax(100 * sim, dim=0)
190
+ else:
191
+ sim /= torch.max(sim) # For visualization
192
+
193
+ sim = sim.reshape(N, H, W)
194
+
195
+ # Resize heatmaps to original resolution for display
196
+ sim_resized = F.interpolate(sim.unsqueeze(0), size=image_size, mode='bilinear', align_corners=False).squeeze(0)
197
+
198
+ yield "Generating outputs..."
199
+ heatmaps = [apply_colormap(sim_resized[i].cpu().numpy()) for i in range(N)]
200
+ yield make_grid_output(heatmaps, prompt_list)
201
+ torch.cuda.empty_cache()
202
+
203
+ def main():
204
+ with gr.Blocks(title="RADSeg 2D Demo") as demo:
205
+ gr.Markdown(
206
+ """
207
+ # RADSeg: Zero-Shot Open-Vocabulary Segmentation
208
+ ### [Paper](https://arxiv.org/abs/2511.19704) | [Project Page](https://radseg-ovss.github.io/)
209
+ Test RADSeg's zero-shot open-vocabulary segmentation capabilities on any image using text prompts !
210
+ """
211
+ )
212
+
213
+ with gr.Row():
214
+ with gr.Column(scale=1):
215
+ input_image = gr.Image(label="Input Image", type="numpy")
216
+
217
+ with gr.Accordion("Model & Inference Settings", open=False):
218
+ with gr.Group():
219
+ gr.Markdown("### 1. Model Scaling")
220
+ scra_scaling = gr.Slider(1.0, 20.0, 10.0, step=1.0, label="SCRA Scaling", info="Self-Correlating Recursive Attention")
221
+ scga_scaling = gr.Slider(1.0, 20.0, 10.0, step=1.0, label="SCGA Scaling", info="Self-Correlating Global Aggregation")
222
+
223
+ with gr.Group():
224
+ gr.Markdown("### 2. Sliding Window")
225
+ use_sliding_window = gr.Checkbox(label="Enable Sliding Window", value=True)
226
+ with gr.Row():
227
+ window_size = gr.Slider(224, 1024, 336, step=16, label="Window Size")
228
+ window_stride = gr.Slider(112, 512, 224, step=16, label="Window Stride")
229
+
230
+ with gr.Row():
231
+ softmax = gr.Checkbox(label="Use softmax", value=True)
232
+ res_slider = gr.Slider(224, 1024, 512, step=32, label="Resolution (Max Side)")
233
+
234
+ with gr.Group():
235
+ gr.Markdown("### 3. Text Prompts")
236
+ with gr.Row():
237
+ prompt = gr.Textbox(
238
+ label="Prompt",
239
+ placeholder="Type a class (e.g. 'car') and press enter",
240
+ scale=4,
241
+ show_label=False
242
+ )
243
+ add_button = gr.Button("+", scale=1)
244
+
245
+ prompt_display = gr.HTML()
246
+
247
+ with gr.Row():
248
+ clear_button = gr.Button("Clear Prompts")
249
+ run_button = gr.Button("Run Segmentation", variant="primary")
250
+
251
+ with gr.Column(scale=2):
252
+ output_html = gr.HTML(label="Segmentation Heatmaps")
253
+
254
+ # Event handlers
255
+ prompt.submit(add_prompt, inputs=prompt, outputs=[prompt, prompt_display])
256
+ add_button.click(add_prompt, inputs=prompt, outputs=[prompt, prompt_display])
257
+ clear_button.click(clear_prompts, outputs=[prompt, prompt_display])
258
+
259
+ gr.Examples(
260
+ examples=[
261
+ ["assets/example1.jpg", 10, 10, True, 336, 224, True, "Pothole\nRoad\nSky\nCar\nWater", 768],
262
+ ["assets/example2.jpg", 10, 10, True, 336, 224, True, "Person\nShoes\nGrey Jacket\nRed overalls\nRoad\nCrosswalk\nCar", 768],
263
+ ["assets/example3.jpg", 10, 10, True, 336, 224, True, "Paved ground\nFlood lights\nRed Container\nBuilding\nTanker\nSky\nClouds\nTreeline", 1024]
264
+ ],
265
+ inputs=[input_image, scra_scaling, scga_scaling, use_sliding_window, window_size, window_stride, softmax, prompt, res_slider],
266
+ )
267
+
268
+ run_button.click(
269
+ fn=process_all,
270
+ inputs=[
271
+ input_image,
272
+ scra_scaling, scga_scaling,
273
+ use_sliding_window, window_size, window_stride,
274
+ softmax, res_slider
275
+ ],
276
+ outputs=output_html
277
+ )
278
+
279
+ demo.load(on_page_load, outputs=prompt_display)
280
+
281
+ demo.queue().launch()
282
+
283
+ if __name__ == "__main__":
284
+ main()
285
+
requirements.txt ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ torch==2.4.0
2
+ torchvision==0.19.0
3
+
4
+ timm==1.0.19
5
+ segment-anything
6
+ opencv-python-headless
7
+ pillow
8
+ numpy
9
+ matplotlib
10
+ ftfy
11
+ regex
12
+ pyyaml
13
+ tqdm
14
+ huggingface-hub
15
+ transformers
16
+ open-clip-torch
17
+ scipy
18
+ scikit-image
19
+ addict
20
+ yapf
21
+ typing_extensions
22
+ einops
23
+ gradio