Spaces:
Running on Zero
Running on Zero
Omar Alama commited on
Commit ·
72374cb
0
Parent(s):
Init
Browse files- .gitattributes +36 -0
- README.md +15 -0
- assets/example1.jpg +3 -0
- assets/example2.jpg +3 -0
- assets/example3.jpg +3 -0
- radseg/base.py +216 -0
- radseg/prompt_templates.py +84 -0
- radseg/radseg.py +547 -0
- radseg/sam_utils.py +174 -0
- radseg_demo.py +285 -0
- requirements.txt +23 -0
.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
|
assets/example2.jpg
ADDED
|
Git LFS Details
|
assets/example3.jpg
ADDED
|
Git LFS Details
|
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
|