Skip to content

Commit befeaed

Browse files
Add SAM3 Promptable Concept Segmentation (PCS) model (keras-team#2534)
* Add SAM3 PCS model. * Rename to PromptableConcept. * Update docstrings and add post-processing. * Remove testing code. Rename to SAM3PromptableConceptImageSegmenterPreprocessor. * Fix quantization. * Address comments. * Add custom image shape support.
1 parent 6e74a74 commit befeaed

26 files changed

Lines changed: 6152 additions & 2 deletions

keras_hub/api/layers/__init__.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,27 @@
138138
from keras_hub.src.models.sam.sam_prompt_encoder import (
139139
SAMPromptEncoder as SAMPromptEncoder,
140140
)
141+
from keras_hub.src.models.sam3.sam3_detr_decoder import (
142+
SAM3DetrDecoder as SAM3DetrDecoder,
143+
)
144+
from keras_hub.src.models.sam3.sam3_detr_encoder import (
145+
SAM3DetrEncoder as SAM3DetrEncoder,
146+
)
147+
from keras_hub.src.models.sam3.sam3_geometry_encoder import (
148+
SAM3GeometryEncoder as SAM3GeometryEncoder,
149+
)
150+
from keras_hub.src.models.sam3.sam3_image_converter import (
151+
SAM3ImageConverter as SAM3ImageConverter,
152+
)
153+
from keras_hub.src.models.sam3.sam3_mask_decoder import (
154+
SAM3MaskDecoder as SAM3MaskDecoder,
155+
)
156+
from keras_hub.src.models.sam3.sam3_text_encoder import (
157+
SAM3TextEncoder as SAM3TextEncoder,
158+
)
159+
from keras_hub.src.models.sam3.sam3_vision_encoder import (
160+
SAM3VisionEncoder as SAM3VisionEncoder,
161+
)
141162
from keras_hub.src.models.segformer.segformer_image_converter import (
142163
SegFormerImageConverter as SegFormerImageConverter,
143164
)

keras_hub/api/models/__init__.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -651,6 +651,18 @@
651651
from keras_hub.src.models.sam.sam_image_segmenter_preprocessor import (
652652
SAMImageSegmenterPreprocessor as SAMImageSegmenterPreprocessor,
653653
)
654+
from keras_hub.src.models.sam3.sam3_pc_backbone import (
655+
SAM3PromptableConceptBackbone as SAM3PromptableConceptBackbone,
656+
)
657+
from keras_hub.src.models.sam3.sam3_pc_image_segmenter import (
658+
SAM3PromptableConceptImageSegmenter as SAM3PromptableConceptImageSegmenter,
659+
)
660+
from keras_hub.src.models.sam3.sam3_pc_image_segmenter_preprocessor import (
661+
SAM3PromptableConceptImageSegmenterPreprocessor as SAM3PromptableConceptImageSegmenterPreprocessor,
662+
)
663+
from keras_hub.src.models.sam3.sam3_tokenizer import (
664+
SAM3Tokenizer as SAM3Tokenizer,
665+
)
654666
from keras_hub.src.models.segformer.segformer_backbone import (
655667
SegFormerBackbone as SegFormerBackbone,
656668
)

keras_hub/api/tokenizers/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,9 @@
9696
from keras_hub.src.models.rwkv7.rwkv7_tokenizer import (
9797
RWKVTokenizer as RWKVTokenizer,
9898
)
99+
from keras_hub.src.models.sam3.sam3_tokenizer import (
100+
SAM3Tokenizer as SAM3Tokenizer,
101+
)
99102
from keras_hub.src.models.siglip.siglip_tokenizer import (
100103
SigLIPTokenizer as SigLIPTokenizer,
101104
)

keras_hub/src/models/sam/sam_backbone.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@ class SAMBackbone(Backbone):
1616
mask_decoder: `keras_hub.layers.SAMMaskDecoder`. A Keras layer to
1717
generate segmentation masks given the embeddings generated by the
1818
backbone and the prompt encoder.
19-
dtype: The dtype of the layer weights.
19+
dtype: string or `keras.mixed_precision.DTypePolicy`. The dtype to use
20+
for the models computations and weights. Note that some
21+
computations, such as softmax and layer normalization will always
22+
be done in float32 precision regardless of dtype. Defaults to
23+
`bfloat16`.
2024
2125
Example:
2226
```python
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from keras_hub.src.models.sam3.sam3_pc_backbone import (
2+
SAM3PromptableConceptBackbone,
3+
)
4+
from keras_hub.src.models.sam3.sam3_presets import backbone_presets
5+
from keras_hub.src.utils.preset_utils import register_presets
6+
7+
register_presets(backbone_presets, SAM3PromptableConceptBackbone)
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
from keras import backend
2+
from keras import ops
3+
4+
5+
def _bilinear_interpolate(
6+
feature_maps, roi_batch_ind, y, x, ymask, xmask, height, width, hidden_dim
7+
):
8+
feature_maps_dtype = backend.standardize_dtype(feature_maps.dtype)
9+
y = ops.maximum(y, 0.0)
10+
x = ops.maximum(x, 0.0)
11+
y_low = ops.cast(y, "int32")
12+
x_low = ops.cast(x, "int32")
13+
y_high = ops.where(
14+
ops.greater_equal(y_low, height - 1), height - 1, y_low + 1
15+
)
16+
y_low = ops.where(ops.greater_equal(y_low, height - 1), height - 1, y_low)
17+
y = ops.where(
18+
ops.greater_equal(y_low, height - 1),
19+
ops.cast(y, dtype=feature_maps_dtype),
20+
y,
21+
)
22+
23+
x_high = ops.where(
24+
ops.greater_equal(x_low, width - 1), width - 1, x_low + 1
25+
)
26+
x_low = ops.where(ops.greater_equal(x_low, width - 1), width - 1, x_low)
27+
x = ops.where(
28+
ops.greater_equal(x_low, width - 1),
29+
ops.cast(x, dtype=feature_maps_dtype),
30+
x,
31+
)
32+
33+
ly = ops.subtract(y, y_low)
34+
lx = ops.subtract(x, x_low)
35+
hy = ops.subtract(1.0, ly)
36+
hx = ops.subtract(1.0, lx)
37+
38+
def masked_index(y, x):
39+
y = ops.where(ymask[:, None, :], y, 0)
40+
x = ops.where(xmask[:, None, :], x, 0)
41+
batch_idx = roi_batch_ind[:, None, None, None, None, None]
42+
channel_idx = ops.arange(hidden_dim)[None, None, None, None, None, :]
43+
y_idx = y[:, :, None, :, None, None]
44+
x_idx = x[:, None, :, None, :, None]
45+
46+
if backend.backend() == "tensorflow":
47+
import tensorflow as tf
48+
49+
# Explicitly broadcast indices to the same shape for XLA
50+
# compatibility
51+
common_zero = ops.zeros_like(
52+
batch_idx + y_idx + x_idx + channel_idx
53+
)
54+
batch_idx = batch_idx + common_zero
55+
y_idx = y_idx + common_zero
56+
x_idx = ops.transpose(
57+
ops.transpose(x_idx, (0, 2, 1, 4, 3, 5)) + common_zero,
58+
(0, 2, 1, 4, 3, 5),
59+
)
60+
channel_idx = channel_idx + common_zero
61+
indices = ops.stack([batch_idx, y_idx, x_idx, channel_idx], axis=-1)
62+
indices = ops.cast(indices, "int32")
63+
return tf.gather_nd(feature_maps, indices)
64+
else:
65+
return feature_maps[
66+
batch_idx,
67+
y_idx,
68+
x_idx,
69+
channel_idx,
70+
]
71+
72+
v1 = masked_index(y_low, x_low)
73+
v2 = masked_index(y_low, x_high)
74+
v3 = masked_index(y_high, x_low)
75+
v4 = masked_index(y_high, x_high)
76+
77+
def outer_prod(y, x):
78+
return ops.multiply(
79+
y[:, :, None, :, None, None], x[:, None, :, None, :, None]
80+
)
81+
82+
w1 = outer_prod(hy, hx)
83+
w2 = outer_prod(hy, lx)
84+
w3 = outer_prod(ly, hx)
85+
w4 = outer_prod(ly, lx)
86+
87+
val = ops.add(
88+
ops.add(ops.multiply(w1, v1), ops.multiply(w2, v2)),
89+
ops.add(ops.multiply(w3, v3), ops.multiply(w4, v4)),
90+
)
91+
return val
92+
93+
94+
def roi_align_torch(
95+
feature_maps,
96+
rois,
97+
output_size,
98+
spatial_scale=1.0,
99+
aligned=False,
100+
):
101+
import torchvision
102+
103+
dtype = backend.standardize_dtype(feature_maps.dtype)
104+
need_cast = False
105+
if dtype == "bfloat16":
106+
# torchvision.ops.roi_align does not support bfloat16.
107+
feature_maps = ops.cast(feature_maps, "float32")
108+
rois = ops.cast(rois, "float32")
109+
need_cast = True
110+
111+
output = ops.transpose(
112+
torchvision.ops.roi_align(
113+
ops.transpose(feature_maps, (0, 3, 1, 2)),
114+
rois,
115+
output_size,
116+
spatial_scale=spatial_scale,
117+
aligned=aligned,
118+
),
119+
(0, 2, 3, 1),
120+
)
121+
if need_cast:
122+
output = ops.cast(output, dtype)
123+
return output
124+
125+
126+
def roi_align(
127+
feature_maps,
128+
rois,
129+
output_size,
130+
height,
131+
width,
132+
hidden_dim,
133+
spatial_scale=1.0,
134+
aligned=False,
135+
):
136+
# Use torchvision's optimized roi_align implementation.
137+
if backend.backend() == "torch":
138+
return roi_align_torch(
139+
feature_maps,
140+
rois,
141+
output_size,
142+
spatial_scale=spatial_scale,
143+
aligned=aligned,
144+
)
145+
146+
original_dtype = backend.standardize_dtype(feature_maps.dtype)
147+
out_h, out_w = output_size[0], output_size[1]
148+
149+
feature_maps = ops.cast(feature_maps, "float32")
150+
rois = ops.cast(rois, "float32")
151+
152+
ph = ops.arange(out_h, dtype="float32")
153+
pw = ops.arange(out_w, dtype="float32")
154+
155+
# input: [N, C, H, W]
156+
# rois: [K, 5]
157+
158+
roi_batch_ind = ops.cast(rois[:, 0], "int32")
159+
offset = 0.5 if aligned else 0.0
160+
roi_start_w = ops.subtract(ops.multiply(rois[:, 1], spatial_scale), offset)
161+
roi_start_h = ops.subtract(ops.multiply(rois[:, 2], spatial_scale), offset)
162+
roi_end_w = ops.subtract(ops.multiply(rois[:, 3], spatial_scale), offset)
163+
roi_end_h = ops.subtract(ops.multiply(rois[:, 4], spatial_scale), offset)
164+
165+
roi_width = ops.subtract(roi_end_w, roi_start_w)
166+
roi_height = ops.subtract(roi_end_h, roi_start_h)
167+
if not aligned:
168+
roi_width = ops.maximum(roi_width, 1.0)
169+
roi_height = ops.maximum(roi_height, 1.0)
170+
171+
bin_size_h = ops.divide(roi_height, out_h)
172+
bin_size_w = ops.divide(roi_width, out_w)
173+
174+
roi_bin_grid_h = ops.ceil(ops.divide(roi_height, out_h))
175+
roi_bin_grid_w = ops.ceil(ops.divide(roi_width, out_w))
176+
177+
count = ops.maximum(ops.multiply(roi_bin_grid_h, roi_bin_grid_w), 1.0)
178+
iy = ops.arange(height, dtype="float32")
179+
ix = ops.arange(width, dtype="float32")
180+
ymask = ops.less(iy[None, :], roi_bin_grid_h[:, None])
181+
xmask = ops.less(ix[None, :], roi_bin_grid_w[:, None])
182+
183+
def from_k(t):
184+
return t[:, None, None]
185+
186+
y = ops.add(
187+
ops.add(
188+
from_k(roi_start_h),
189+
ops.multiply(ph[None, :, None], from_k(bin_size_h)),
190+
),
191+
ops.multiply(
192+
ops.cast(ops.add(iy[None, None, :], 0.5), dtype="float32"),
193+
from_k(ops.divide(bin_size_h, roi_bin_grid_h)),
194+
),
195+
)
196+
x = ops.add(
197+
ops.add(
198+
from_k(roi_start_w),
199+
ops.multiply(pw[None, :, None], from_k(bin_size_w)),
200+
),
201+
ops.multiply(
202+
ops.cast(ops.add(ix[None, None, :], 0.5), dtype="float32"),
203+
from_k(ops.divide(bin_size_w, roi_bin_grid_w)),
204+
),
205+
)
206+
val = _bilinear_interpolate(
207+
feature_maps,
208+
roi_batch_ind,
209+
y,
210+
x,
211+
ymask,
212+
xmask,
213+
height,
214+
width,
215+
hidden_dim,
216+
)
217+
val = ops.where(ymask[:, None, None, :, None, None], val, 0.0)
218+
val = ops.where(xmask[:, None, None, None, :, None], val, 0.0)
219+
220+
output = ops.sum(val, axis=(3, 4))
221+
output = ops.divide(output, count[:, None, None, None])
222+
return ops.cast(output, original_dtype)

0 commit comments

Comments
 (0)