feat: initial HSAP platform

Huaxu Sentinel Active Safety Platform with embedded algorithm code,
Docker Compose setup, and vendored dataset scaffolds for clone-and-run.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-25 16:59:59 +08:00
commit 7c43b44c57
1619 changed files with 373355 additions and 0 deletions

View File

@@ -0,0 +1,25 @@
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
from .model import SAM
from .predict import (
Predictor,
SAM2DynamicInteractivePredictor,
SAM2Predictor,
SAM2VideoPredictor,
SAM3Predictor,
SAM3SemanticPredictor,
SAM3VideoPredictor,
SAM3VideoSemanticPredictor,
)
__all__ = (
"SAM",
"Predictor",
"SAM2DynamicInteractivePredictor",
"SAM2Predictor",
"SAM2VideoPredictor",
"SAM3Predictor",
"SAM3SemanticPredictor",
"SAM3VideoPredictor",
"SAM3VideoSemanticPredictor",
) # tuple or list of exportable items

View File

@@ -0,0 +1,275 @@
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
from __future__ import annotations
import math
from collections.abc import Generator
from itertools import product
from typing import Any
import numpy as np
import torch
def is_box_near_crop_edge(
boxes: torch.Tensor, crop_box: list[int], orig_box: list[int], atol: float = 20.0
) -> torch.Tensor:
"""Determine if bounding boxes are near the edge of a cropped image region using a specified tolerance.
Args:
boxes (torch.Tensor): Bounding boxes in XYXY format.
crop_box (list[int]): Crop box coordinates in [x0, y0, x1, y1] format.
orig_box (list[int]): Original image box coordinates in [x0, y0, x1, y1] format.
atol (float, optional): Absolute tolerance for edge proximity detection.
Returns:
(torch.Tensor): Boolean tensor indicating which boxes are near crop edges.
Examples:
>>> boxes = torch.tensor([[10, 10, 50, 50], [100, 100, 150, 150]])
>>> crop_box = [0, 0, 200, 200]
>>> orig_box = [0, 0, 300, 300]
>>> near_edge = is_box_near_crop_edge(boxes, crop_box, orig_box, atol=20.0)
"""
crop_box_torch = torch.as_tensor(crop_box, dtype=torch.float, device=boxes.device)
orig_box_torch = torch.as_tensor(orig_box, dtype=torch.float, device=boxes.device)
boxes = uncrop_boxes_xyxy(boxes, crop_box).float()
near_crop_edge = torch.isclose(boxes, crop_box_torch[None, :], atol=atol, rtol=0)
near_image_edge = torch.isclose(boxes, orig_box_torch[None, :], atol=atol, rtol=0)
near_crop_edge = torch.logical_and(near_crop_edge, ~near_image_edge)
return torch.any(near_crop_edge, dim=1)
def batch_iterator(batch_size: int, *args) -> Generator[list[Any]]:
"""Yield batches of data from input arguments with specified batch size for efficient processing.
This function takes a batch size and any number of iterables, then yields batches of elements from those
iterables. All input iterables must have the same length.
Args:
batch_size (int): Size of each batch to yield.
*args (Any): Variable length input iterables to batch. All iterables must have the same length.
Yields:
(list[Any]): A list of batched elements from each input iterable.
Examples:
>>> data = [1, 2, 3, 4, 5]
>>> labels = ["a", "b", "c", "d", "e"]
>>> for batch in batch_iterator(2, data, labels):
... print(batch)
[[1, 2], ['a', 'b']]
[[3, 4], ['c', 'd']]
[[5], ['e']]
"""
assert args and all(len(a) == len(args[0]) for a in args), "Batched iteration must have same-size inputs."
n_batches = len(args[0]) // batch_size + int(len(args[0]) % batch_size != 0)
for b in range(n_batches):
yield [arg[b * batch_size : (b + 1) * batch_size] for arg in args]
def calculate_stability_score(masks: torch.Tensor, mask_threshold: float, threshold_offset: float) -> torch.Tensor:
"""Compute the stability score for a batch of masks.
The stability score is the IoU between binary masks obtained by thresholding the predicted mask logits at high and
low values.
Args:
masks (torch.Tensor): Batch of predicted mask logits.
mask_threshold (float): Threshold value for creating binary masks.
threshold_offset (float): Offset applied to the threshold for creating high and low binary masks.
Returns:
(torch.Tensor): Stability scores for each mask in the batch.
Examples:
>>> masks = torch.rand(10, 256, 256) # Batch of 10 masks
>>> mask_threshold = 0.5
>>> threshold_offset = 0.1
>>> stability_scores = calculate_stability_score(masks, mask_threshold, threshold_offset)
Notes:
- One mask is always contained inside the other.
- Memory is saved by preventing unnecessary cast to torch.int64.
"""
intersections = (masks > (mask_threshold + threshold_offset)).sum(-1, dtype=torch.int16).sum(-1, dtype=torch.int32)
unions = (masks > (mask_threshold - threshold_offset)).sum(-1, dtype=torch.int16).sum(-1, dtype=torch.int32)
return intersections / unions
def build_point_grid(n_per_side: int) -> np.ndarray:
"""Generate a 2D grid of evenly spaced points in the range [0,1]x[0,1] for image segmentation tasks."""
offset = 1 / (2 * n_per_side)
points_one_side = np.linspace(offset, 1 - offset, n_per_side)
points_x = np.tile(points_one_side[None, :], (n_per_side, 1))
points_y = np.tile(points_one_side[:, None], (1, n_per_side))
return np.stack([points_x, points_y], axis=-1).reshape(-1, 2)
def build_all_layer_point_grids(n_per_side: int, n_layers: int, scale_per_layer: int) -> list[np.ndarray]:
"""Generate point grids for multiple crop layers with varying scales and densities."""
return [build_point_grid(int(n_per_side / (scale_per_layer**i))) for i in range(n_layers + 1)]
def generate_crop_boxes(
im_size: tuple[int, ...], n_layers: int, overlap_ratio: float
) -> tuple[list[list[int]], list[int]]:
"""Generate crop boxes of varying sizes for multiscale image processing, with layered overlapping regions.
Args:
im_size (tuple[int, ...]): Height and width of the input image.
n_layers (int): Number of layers to generate crop boxes for.
overlap_ratio (float): Ratio of overlap between adjacent crop boxes.
Returns:
crop_boxes (list[list[int]]): List of crop boxes in [x0, y0, x1, y1] format.
layer_idxs (list[int]): List of layer indices corresponding to each crop box.
Examples:
>>> im_size = (800, 1200) # Height, width
>>> n_layers = 3
>>> overlap_ratio = 0.25
>>> crop_boxes, layer_idxs = generate_crop_boxes(im_size, n_layers, overlap_ratio)
"""
crop_boxes, layer_idxs = [], []
im_h, im_w = im_size
short_side = min(im_h, im_w)
# Original image
crop_boxes.append([0, 0, im_w, im_h])
layer_idxs.append(0)
def crop_len(orig_len, n_crops, overlap):
"""Calculate the length of each crop given the original length, number of crops, and overlap."""
return math.ceil((overlap * (n_crops - 1) + orig_len) / n_crops)
for i_layer in range(n_layers):
n_crops_per_side = 2 ** (i_layer + 1)
overlap = int(overlap_ratio * short_side * (2 / n_crops_per_side))
crop_w = crop_len(im_w, n_crops_per_side, overlap)
crop_h = crop_len(im_h, n_crops_per_side, overlap)
crop_box_x0 = [int((crop_w - overlap) * i) for i in range(n_crops_per_side)]
crop_box_y0 = [int((crop_h - overlap) * i) for i in range(n_crops_per_side)]
# Crops in XYWH format
for x0, y0 in product(crop_box_x0, crop_box_y0):
box = [x0, y0, min(x0 + crop_w, im_w), min(y0 + crop_h, im_h)]
crop_boxes.append(box)
layer_idxs.append(i_layer + 1)
return crop_boxes, layer_idxs
def uncrop_boxes_xyxy(boxes: torch.Tensor, crop_box: list[int]) -> torch.Tensor:
"""Uncrop bounding boxes by adding the crop box offset to their coordinates."""
x0, y0, _, _ = crop_box
offset = torch.tensor([[x0, y0, x0, y0]], device=boxes.device)
# Check if boxes has a channel dimension
if len(boxes.shape) == 3:
offset = offset.unsqueeze(1)
return boxes + offset
def uncrop_points(points: torch.Tensor, crop_box: list[int]) -> torch.Tensor:
"""Uncrop points by adding the crop box offset to their coordinates."""
x0, y0, _, _ = crop_box
offset = torch.tensor([[x0, y0]], device=points.device)
# Check if points has a channel dimension
if len(points.shape) == 3:
offset = offset.unsqueeze(1)
return points + offset
def uncrop_masks(masks: torch.Tensor, crop_box: list[int], orig_h: int, orig_w: int) -> torch.Tensor:
"""Uncrop masks by padding them to the original image size, handling coordinate transformations."""
x0, y0, x1, y1 = crop_box
if x0 == 0 and y0 == 0 and x1 == orig_w and y1 == orig_h:
return masks
# Coordinate transform masks
pad_x, pad_y = orig_w - (x1 - x0), orig_h - (y1 - y0)
pad = (x0, pad_x - x0, y0, pad_y - y0)
return torch.nn.functional.pad(masks, pad, value=0)
def remove_small_regions(mask: np.ndarray, area_thresh: float, mode: str) -> tuple[np.ndarray, bool]:
"""Remove small disconnected regions or holes in a mask based on area threshold and mode.
Args:
mask (np.ndarray): Binary mask to process.
area_thresh (float): Area threshold below which regions will be removed.
mode (str): Processing mode, either 'holes' to fill small holes or 'islands' to remove small disconnected
regions.
Returns:
processed_mask (np.ndarray): Processed binary mask with small regions removed.
modified (bool): Whether any regions were modified.
Examples:
>>> mask = np.zeros((100, 100), dtype=np.bool_)
>>> mask[40:60, 40:60] = True # Create a square
>>> mask[45:55, 45:55] = False # Create a hole
>>> processed_mask, modified = remove_small_regions(mask, 50, "holes")
"""
import cv2 # type: ignore
assert mode in {"holes", "islands"}, f"Provided mode {mode} is invalid"
correct_holes = mode == "holes"
working_mask = (correct_holes ^ mask).astype(np.uint8)
n_labels, regions, stats, _ = cv2.connectedComponentsWithStats(working_mask, 8)
sizes = stats[:, -1][1:] # Row 0 is background label
small_regions = [i + 1 for i, s in enumerate(sizes) if s < area_thresh]
if not small_regions:
return mask, False
fill_labels = [0, *small_regions]
if not correct_holes:
# If every region is below threshold, keep largest
fill_labels = [i for i in range(n_labels) if i not in fill_labels] or [int(np.argmax(sizes)) + 1]
mask = np.isin(regions, fill_labels)
return mask, True
def batched_mask_to_box(masks: torch.Tensor) -> torch.Tensor:
"""Calculate bounding boxes in XYXY format around binary masks.
Args:
masks (torch.Tensor): Binary masks with shape (B, H, W) or (B, C, H, W).
Returns:
(torch.Tensor): Bounding boxes in XYXY format with shape (B, 4) or (B, C, 4).
Notes:
- Handles empty masks by returning zero boxes.
- Preserves input tensor dimensions in the output.
"""
# torch.max below raises an error on empty inputs, just skip in this case
if torch.numel(masks) == 0:
return torch.zeros(*masks.shape[:-2], 4, device=masks.device)
# Normalize shape to CxHxW
shape = masks.shape
h, w = shape[-2:]
masks = masks.flatten(0, -3) if len(shape) > 2 else masks.unsqueeze(0)
# Get top and bottom edges
in_height, _ = torch.max(masks, dim=-1)
in_height_coords = in_height * torch.arange(h, device=in_height.device)[None, :]
bottom_edges, _ = torch.max(in_height_coords, dim=-1)
in_height_coords = in_height_coords + h * (~in_height)
top_edges, _ = torch.min(in_height_coords, dim=-1)
# Get left and right edges
in_width, _ = torch.max(masks, dim=-2)
in_width_coords = in_width * torch.arange(w, device=in_width.device)[None, :]
right_edges, _ = torch.max(in_width_coords, dim=-1)
in_width_coords = in_width_coords + w * (~in_width)
left_edges, _ = torch.min(in_width_coords, dim=-1)
# If the mask is empty the right edge will be to the left of the left edge.
# Replace these boxes with [0, 0, 0, 0]
empty_filter = (right_edges < left_edges) | (bottom_edges < top_edges)
out = torch.stack([left_edges, top_edges, right_edges, bottom_edges], dim=-1)
out = out * (~empty_filter).unsqueeze(-1)
# Return to original shape
return out.reshape(*shape[:-2], 4) if len(shape) > 2 else out[0]

View File

@@ -0,0 +1,365 @@
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
# Copyright (c) Meta Platforms, Inc. and affiliates.
# All rights reserved.
# This source code is licensed under the license found in the
# LICENSE file in the root directory of this source tree.
from functools import partial
import torch
from ultralytics.utils.downloads import attempt_download_asset
from ultralytics.utils.patches import torch_load
from .modules.decoders import MaskDecoder
from .modules.encoders import FpnNeck, Hiera, ImageEncoder, ImageEncoderViT, MemoryEncoder, PromptEncoder
from .modules.memory_attention import MemoryAttention, MemoryAttentionLayer
from .modules.sam import SAM2Model, SAMModel
from .modules.tiny_encoder import TinyViT
from .modules.transformer import TwoWayTransformer
def _load_checkpoint(model, checkpoint):
"""Load checkpoint into model from file path."""
if checkpoint is None:
return model
checkpoint = attempt_download_asset(checkpoint)
with open(checkpoint, "rb") as f:
state_dict = torch_load(f)
# Handle nested "model" key
if "model" in state_dict and isinstance(state_dict["model"], dict):
state_dict = state_dict["model"]
model.load_state_dict(state_dict)
return model
def build_sam_vit_h(checkpoint=None):
"""Build and return a Segment Anything Model (SAM) h-size model with specified encoder parameters."""
return _build_sam(
encoder_embed_dim=1280,
encoder_depth=32,
encoder_num_heads=16,
encoder_global_attn_indexes=[7, 15, 23, 31],
checkpoint=checkpoint,
)
def build_sam_vit_l(checkpoint=None):
"""Build and return a Segment Anything Model (SAM) l-size model with specified encoder parameters."""
return _build_sam(
encoder_embed_dim=1024,
encoder_depth=24,
encoder_num_heads=16,
encoder_global_attn_indexes=[5, 11, 17, 23],
checkpoint=checkpoint,
)
def build_sam_vit_b(checkpoint=None):
"""Build and return a Segment Anything Model (SAM) b-size model with specified encoder parameters."""
return _build_sam(
encoder_embed_dim=768,
encoder_depth=12,
encoder_num_heads=12,
encoder_global_attn_indexes=[2, 5, 8, 11],
checkpoint=checkpoint,
)
def build_mobile_sam(checkpoint=None):
"""Build and return a Mobile Segment Anything Model (Mobile-SAM) for efficient image segmentation."""
return _build_sam(
encoder_embed_dim=[64, 128, 160, 320],
encoder_depth=[2, 2, 6, 2],
encoder_num_heads=[2, 4, 5, 10],
encoder_global_attn_indexes=None,
mobile_sam=True,
checkpoint=checkpoint,
)
def build_sam2_t(checkpoint=None):
"""Build and return a Segment Anything Model 2 (SAM2) tiny-size model with specified architecture parameters."""
return _build_sam2(
encoder_embed_dim=96,
encoder_stages=[1, 2, 7, 2],
encoder_num_heads=1,
encoder_global_att_blocks=[5, 7, 9],
encoder_window_spec=[8, 4, 14, 7],
encoder_backbone_channel_list=[768, 384, 192, 96],
checkpoint=checkpoint,
)
def build_sam2_s(checkpoint=None):
"""Build and return a small-size Segment Anything Model 2 (SAM2) with specified architecture parameters."""
return _build_sam2(
encoder_embed_dim=96,
encoder_stages=[1, 2, 11, 2],
encoder_num_heads=1,
encoder_global_att_blocks=[7, 10, 13],
encoder_window_spec=[8, 4, 14, 7],
encoder_backbone_channel_list=[768, 384, 192, 96],
checkpoint=checkpoint,
)
def build_sam2_b(checkpoint=None):
"""Build and return a Segment Anything Model 2 (SAM2) base-size model with specified architecture parameters."""
return _build_sam2(
encoder_embed_dim=112,
encoder_stages=[2, 3, 16, 3],
encoder_num_heads=2,
encoder_global_att_blocks=[12, 16, 20],
encoder_window_spec=[8, 4, 14, 7],
encoder_window_spatial_size=[14, 14],
encoder_backbone_channel_list=[896, 448, 224, 112],
checkpoint=checkpoint,
)
def build_sam2_l(checkpoint=None):
"""Build and return a large-size Segment Anything Model 2 (SAM2) with specified architecture parameters."""
return _build_sam2(
encoder_embed_dim=144,
encoder_stages=[2, 6, 36, 4],
encoder_num_heads=2,
encoder_global_att_blocks=[23, 33, 43],
encoder_window_spec=[8, 4, 16, 8],
encoder_backbone_channel_list=[1152, 576, 288, 144],
checkpoint=checkpoint,
)
def _build_sam(
encoder_embed_dim,
encoder_depth,
encoder_num_heads,
encoder_global_attn_indexes,
checkpoint=None,
mobile_sam=False,
):
"""Build a Segment Anything Model (SAM) with specified encoder parameters.
Args:
encoder_embed_dim (int | list[int]): Embedding dimension for the encoder.
encoder_depth (int | list[int]): Depth of the encoder.
encoder_num_heads (int | list[int]): Number of attention heads in the encoder.
encoder_global_attn_indexes (list[int] | None): Indexes for global attention in the encoder.
checkpoint (str | None, optional): Path to the model checkpoint file.
mobile_sam (bool, optional): Whether to build a Mobile-SAM model.
Returns:
(SAMModel): A Segment Anything Model instance with the specified architecture.
Examples:
>>> sam = _build_sam(768, 12, 12, [2, 5, 8, 11])
>>> sam = _build_sam([64, 128, 160, 320], [2, 2, 6, 2], [2, 4, 5, 10], None, mobile_sam=True)
"""
prompt_embed_dim = 256
image_size = 1024
vit_patch_size = 16
image_embedding_size = image_size // vit_patch_size
image_encoder = (
TinyViT(
img_size=1024,
in_chans=3,
num_classes=1000,
embed_dims=encoder_embed_dim,
depths=encoder_depth,
num_heads=encoder_num_heads,
window_sizes=[7, 7, 14, 7],
mlp_ratio=4.0,
drop_rate=0.0,
drop_path_rate=0.0,
use_checkpoint=False,
mbconv_expand_ratio=4.0,
local_conv_size=3,
layer_lr_decay=0.8,
)
if mobile_sam
else ImageEncoderViT(
depth=encoder_depth,
embed_dim=encoder_embed_dim,
img_size=image_size,
mlp_ratio=4,
norm_layer=partial(torch.nn.LayerNorm, eps=1e-6),
num_heads=encoder_num_heads,
patch_size=vit_patch_size,
qkv_bias=True,
use_rel_pos=True,
global_attn_indexes=encoder_global_attn_indexes,
window_size=14,
out_chans=prompt_embed_dim,
)
)
sam = SAMModel(
image_encoder=image_encoder,
prompt_encoder=PromptEncoder(
embed_dim=prompt_embed_dim,
image_embedding_size=(image_embedding_size, image_embedding_size),
input_image_size=(image_size, image_size),
mask_in_chans=16,
),
mask_decoder=MaskDecoder(
num_multimask_outputs=3,
transformer=TwoWayTransformer(
depth=2,
embedding_dim=prompt_embed_dim,
mlp_dim=2048,
num_heads=8,
),
transformer_dim=prompt_embed_dim,
iou_head_depth=3,
iou_head_hidden_dim=256,
),
pixel_mean=[123.675, 116.28, 103.53],
pixel_std=[58.395, 57.12, 57.375],
)
if checkpoint is not None:
sam = _load_checkpoint(sam, checkpoint)
sam.eval()
return sam
def _build_sam2(
encoder_embed_dim=1280,
encoder_stages=(2, 6, 36, 4),
encoder_num_heads=2,
encoder_global_att_blocks=(7, 15, 23, 31),
encoder_backbone_channel_list=(1152, 576, 288, 144),
encoder_window_spatial_size=(7, 7),
encoder_window_spec=(8, 4, 16, 8),
checkpoint=None,
):
"""Build and return a Segment Anything Model 2 (SAM2) with specified architecture parameters.
Args:
encoder_embed_dim (int, optional): Embedding dimension for the encoder.
encoder_stages (list[int], optional): Number of blocks in each stage of the encoder.
encoder_num_heads (int, optional): Number of attention heads in the encoder.
encoder_global_att_blocks (list[int], optional): Indices of global attention blocks in the encoder.
encoder_backbone_channel_list (list[int], optional): Channel dimensions for each level of the encoder backbone.
encoder_window_spatial_size (list[int], optional): Spatial size of the window for position embeddings.
encoder_window_spec (list[int], optional): Window specifications for each stage of the encoder.
checkpoint (str | None, optional): Path to the checkpoint file for loading pre-trained weights.
Returns:
(SAM2Model): A configured and initialized SAM2 model.
Examples:
>>> sam2_model = _build_sam2(encoder_embed_dim=96, encoder_stages=[1, 2, 7, 2])
>>> sam2_model.eval()
"""
image_encoder = ImageEncoder(
trunk=Hiera(
embed_dim=encoder_embed_dim,
num_heads=encoder_num_heads,
stages=encoder_stages,
global_att_blocks=encoder_global_att_blocks,
window_pos_embed_bkg_spatial_size=encoder_window_spatial_size,
window_spec=encoder_window_spec,
),
neck=FpnNeck(
d_model=256,
backbone_channel_list=encoder_backbone_channel_list,
fpn_top_down_levels=[2, 3],
fpn_interp_model="nearest",
),
scalp=1,
)
memory_attention = MemoryAttention(d_model=256, pos_enc_at_input=True, num_layers=4, layer=MemoryAttentionLayer())
memory_encoder = MemoryEncoder(out_dim=64)
is_sam2_1 = checkpoint is not None and "sam2.1" in checkpoint
sam2 = SAM2Model(
image_encoder=image_encoder,
memory_attention=memory_attention,
memory_encoder=memory_encoder,
num_maskmem=7,
image_size=1024,
sigmoid_scale_for_mem_enc=20.0,
sigmoid_bias_for_mem_enc=-10.0,
use_mask_input_as_output_without_sam=True,
directly_add_no_mem_embed=True,
use_high_res_features_in_sam=True,
multimask_output_in_sam=True,
iou_prediction_use_sigmoid=True,
use_obj_ptrs_in_encoder=True,
add_tpos_enc_to_obj_ptrs=True,
only_obj_ptrs_in_the_past_for_eval=True,
pred_obj_scores=True,
pred_obj_scores_mlp=True,
fixed_no_obj_ptr=True,
multimask_output_for_tracking=True,
use_multimask_token_for_obj_ptr=True,
multimask_min_pt_num=0,
multimask_max_pt_num=1,
use_mlp_for_obj_ptr_proj=True,
compile_image_encoder=False,
no_obj_embed_spatial=is_sam2_1,
proj_tpos_enc_in_obj_ptrs=is_sam2_1,
use_signed_tpos_enc_to_obj_ptrs=is_sam2_1,
sam_mask_decoder_extra_args=dict(
dynamic_multimask_via_stability=True,
dynamic_multimask_stability_delta=0.05,
dynamic_multimask_stability_thresh=0.98,
),
)
if checkpoint is not None:
sam2 = _load_checkpoint(sam2, checkpoint)
sam2.eval()
return sam2
sam_model_map = {
"sam_h.pt": build_sam_vit_h,
"sam_l.pt": build_sam_vit_l,
"sam_b.pt": build_sam_vit_b,
"mobile_sam.pt": build_mobile_sam,
"sam2_t.pt": build_sam2_t,
"sam2_s.pt": build_sam2_s,
"sam2_b.pt": build_sam2_b,
"sam2_l.pt": build_sam2_l,
"sam2.1_t.pt": build_sam2_t,
"sam2.1_s.pt": build_sam2_s,
"sam2.1_b.pt": build_sam2_b,
"sam2.1_l.pt": build_sam2_l,
}
def build_sam(ckpt="sam_b.pt"):
"""Build and return a Segment Anything Model (SAM) based on the provided checkpoint.
Args:
ckpt (str | Path, optional): Path to the checkpoint file or name of a pre-defined SAM model.
Returns:
(SAMModel | SAM2Model): A configured and initialized SAM or SAM2 model instance.
Raises:
FileNotFoundError: If the provided checkpoint is not a supported SAM model.
Examples:
>>> sam_model = build_sam("sam_b.pt")
>>> sam_model = build_sam("path/to/custom_checkpoint.pt")
Notes:
Supported pre-defined models include:
- SAM: 'sam_h.pt', 'sam_l.pt', 'sam_b.pt', 'mobile_sam.pt'
- SAM2: 'sam2_t.pt', 'sam2_s.pt', 'sam2_b.pt', 'sam2_l.pt'
"""
model_builder = None
ckpt = str(ckpt) # to allow Path ckpt types
for k in sam_model_map.keys():
if ckpt.endswith(k):
model_builder = sam_model_map.get(k)
if not model_builder:
raise FileNotFoundError(f"{ckpt} is not a supported SAM model. Available models are: \n {sam_model_map.keys()}")
return model_builder(ckpt)

View File

@@ -0,0 +1,377 @@
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
import torch.nn as nn
from ultralytics.nn.modules.transformer import MLP
from ultralytics.utils.patches import torch_load
from .modules.blocks import PositionEmbeddingSine, RoPEAttention
from .modules.encoders import MemoryEncoder
from .modules.memory_attention import MemoryAttention, MemoryAttentionLayer
from .modules.sam import SAM3Model
from .sam3.decoder import TransformerDecoder, TransformerDecoderLayer
from .sam3.encoder import TransformerEncoderFusion, TransformerEncoderLayer
from .sam3.geometry_encoders import SequenceGeometryEncoder
from .sam3.maskformer_segmentation import PixelDecoder, UniversalSegmentationHead
from .sam3.model_misc import DotProductScoring, TransformerWrapper
from .sam3.necks import Sam3DualViTDetNeck
from .sam3.sam3_image import SAM3SemanticModel
from .sam3.text_encoder_ve import VETextEncoder
from .sam3.vitdet import ViT
from .sam3.vl_combiner import SAM3VLBackbone
def _create_vision_backbone(compile_mode=None, enable_inst_interactivity=True) -> Sam3DualViTDetNeck:
"""Create SAM3 visual backbone with ViT and neck."""
# Position encoding
position_encoding = PositionEmbeddingSine(
num_pos_feats=256,
normalize=True,
scale=None,
temperature=10000,
)
# ViT backbone
vit_backbone = ViT(
img_size=1008,
pretrain_img_size=336,
patch_size=14,
embed_dim=1024,
depth=32,
num_heads=16,
mlp_ratio=4.625,
norm_layer="LayerNorm",
drop_path_rate=0.1,
qkv_bias=True,
use_abs_pos=True,
tile_abs_pos=True,
global_att_blocks=(7, 15, 23, 31),
rel_pos_blocks=(),
use_rope=True,
use_interp_rope=True,
window_size=24,
pretrain_use_cls_token=True,
retain_cls_token=False,
ln_pre=True,
ln_post=False,
return_interm_layers=False,
bias_patch_embed=False,
compile_mode=compile_mode,
)
return Sam3DualViTDetNeck(
position_encoding=position_encoding,
d_model=256,
scale_factors=[4.0, 2.0, 1.0, 0.5],
trunk=vit_backbone,
add_sam2_neck=enable_inst_interactivity,
)
def _create_sam3_transformer() -> TransformerWrapper:
"""Create SAM3 detector encoder and decoder."""
encoder: TransformerEncoderFusion = TransformerEncoderFusion(
layer=TransformerEncoderLayer(
d_model=256,
dim_feedforward=2048,
dropout=0.1,
pos_enc_at_attn=True,
pos_enc_at_cross_attn_keys=False,
pos_enc_at_cross_attn_queries=False,
pre_norm=True,
self_attention=nn.MultiheadAttention(
num_heads=8,
dropout=0.1,
embed_dim=256,
batch_first=True,
),
cross_attention=nn.MultiheadAttention(
num_heads=8,
dropout=0.1,
embed_dim=256,
batch_first=True,
),
),
num_layers=6,
d_model=256,
num_feature_levels=1,
frozen=False,
use_act_checkpoint=True,
add_pooled_text_to_img_feat=False,
pool_text_with_mask=True,
)
decoder: TransformerDecoder = TransformerDecoder(
layer=TransformerDecoderLayer(
d_model=256,
dim_feedforward=2048,
dropout=0.1,
cross_attention=nn.MultiheadAttention(
num_heads=8,
dropout=0.1,
embed_dim=256,
),
n_heads=8,
use_text_cross_attention=True,
),
num_layers=6,
num_queries=200,
return_intermediate=True,
box_refine=True,
num_o2m_queries=0,
dac=True,
boxRPB="log",
d_model=256,
frozen=False,
interaction_layer=None,
dac_use_selfatt_ln=True,
use_act_checkpoint=True,
presence_token=True,
)
return TransformerWrapper(encoder=encoder, decoder=decoder, d_model=256)
def build_sam3_image_model(checkpoint_path: str, enable_segmentation: bool = True, compile: bool = False):
"""Build SAM3 image model.
Args:
checkpoint_path: Optional path to model checkpoint
enable_segmentation: Whether to enable segmentation head
compile: To enable compilation, set to "default"
Returns:
A SAM3 image model
"""
try:
import clip
except ImportError:
from ultralytics.utils.checks import check_requirements
check_requirements("git+https://github.com/ultralytics/CLIP.git")
import clip
# Create visual components
compile_mode = "default" if compile else None
vision_encoder = _create_vision_backbone(compile_mode=compile_mode, enable_inst_interactivity=True)
# Create text components
text_encoder = VETextEncoder(
tokenizer=clip.simple_tokenizer.SimpleTokenizer(),
d_model=256,
width=1024,
heads=16,
layers=24,
)
# Create visual-language backbone
backbone = SAM3VLBackbone(visual=vision_encoder, text=text_encoder, scalp=1)
# Create transformer components
transformer = _create_sam3_transformer()
# Create dot product scoring
dot_prod_scoring = DotProductScoring(
d_model=256,
d_proj=256,
prompt_mlp=MLP(
input_dim=256,
hidden_dim=2048,
output_dim=256,
num_layers=2,
residual=True,
out_norm=nn.LayerNorm(256),
),
)
# Create segmentation head if enabled
segmentation_head = (
UniversalSegmentationHead(
hidden_dim=256,
upsampling_stages=3,
aux_masks=False,
presence_head=False,
dot_product_scorer=None,
act_ckpt=True,
cross_attend_prompt=nn.MultiheadAttention(
num_heads=8,
dropout=0,
embed_dim=256,
),
pixel_decoder=PixelDecoder(
num_upsampling_stages=3,
interpolation_mode="nearest",
hidden_dim=256,
compile_mode=compile_mode,
),
)
if enable_segmentation
else None
)
# Create geometry encoder
input_geometry_encoder = SequenceGeometryEncoder(
pos_enc=PositionEmbeddingSine(
num_pos_feats=256,
normalize=True,
scale=None,
temperature=10000,
),
encode_boxes_as_points=False,
boxes_direct_project=True,
boxes_pool=True,
boxes_pos_enc=True,
d_model=256,
num_layers=3,
layer=TransformerEncoderLayer(
d_model=256,
dim_feedforward=2048,
dropout=0.1,
pos_enc_at_attn=False,
pre_norm=True,
pos_enc_at_cross_attn_queries=False,
pos_enc_at_cross_attn_keys=True,
),
use_act_ckpt=True,
add_cls=True,
add_post_encode_proj=True,
)
# Create the SAM3SemanticModel model
model = SAM3SemanticModel(
backbone=backbone,
transformer=transformer,
input_geometry_encoder=input_geometry_encoder,
segmentation_head=segmentation_head,
num_feature_levels=1,
o2m_mask_predict=True,
dot_prod_scoring=dot_prod_scoring,
use_instance_query=False,
multimask_output=True,
)
# Load checkpoint
model = _load_checkpoint(model, checkpoint_path)
model.eval()
return model
def build_interactive_sam3(checkpoint_path: str, compile=None, with_backbone=True) -> SAM3Model:
"""Build the SAM3 Tracker module for video tracking.
Returns:
Sam3TrackerPredictor: Wrapped SAM3 Tracker module
"""
# Create model components
memory_encoder = MemoryEncoder(out_dim=64, interpol_size=[1152, 1152])
memory_attention = MemoryAttention(
batch_first=True,
d_model=256,
pos_enc_at_input=True,
layer=MemoryAttentionLayer(
dim_feedforward=2048,
dropout=0.1,
pos_enc_at_attn=False,
pos_enc_at_cross_attn_keys=True,
pos_enc_at_cross_attn_queries=False,
self_attn=RoPEAttention(
embedding_dim=256,
num_heads=1,
downsample_rate=1,
rope_theta=10000.0,
feat_sizes=[72, 72],
),
d_model=256,
cross_attn=RoPEAttention(
embedding_dim=256,
num_heads=1,
downsample_rate=1,
kv_in_dim=64,
rope_theta=10000.0,
feat_sizes=[72, 72],
rope_k_repeat=True,
),
),
num_layers=4,
)
backbone = (
SAM3VLBackbone(scalp=1, visual=_create_vision_backbone(compile_mode=compile), text=None)
if with_backbone
else None
)
model = SAM3Model(
image_size=1008,
image_encoder=backbone,
memory_attention=memory_attention,
memory_encoder=memory_encoder,
backbone_stride=14,
num_maskmem=7,
sigmoid_scale_for_mem_enc=20.0,
sigmoid_bias_for_mem_enc=-10.0,
use_mask_input_as_output_without_sam=True,
directly_add_no_mem_embed=True,
use_high_res_features_in_sam=True,
multimask_output_in_sam=True,
iou_prediction_use_sigmoid=True,
use_obj_ptrs_in_encoder=True,
add_tpos_enc_to_obj_ptrs=True,
only_obj_ptrs_in_the_past_for_eval=True,
pred_obj_scores=True,
pred_obj_scores_mlp=True,
fixed_no_obj_ptr=True,
multimask_output_for_tracking=True,
use_multimask_token_for_obj_ptr=True,
multimask_min_pt_num=0,
multimask_max_pt_num=1,
use_mlp_for_obj_ptr_proj=True,
compile_image_encoder=False,
no_obj_embed_spatial=True,
proj_tpos_enc_in_obj_ptrs=True,
use_signed_tpos_enc_to_obj_ptrs=True,
sam_mask_decoder_extra_args=dict(
dynamic_multimask_via_stability=True,
dynamic_multimask_stability_delta=0.05,
dynamic_multimask_stability_thresh=0.98,
),
)
# Load checkpoint if provided
model = _load_checkpoint(model, checkpoint_path, interactive=True)
# Setup device and mode
model.eval()
return model
def _load_checkpoint(model, checkpoint, interactive=False):
"""Load SAM3 model checkpoint from file."""
with open(checkpoint, "rb") as f:
ckpt = torch_load(f)
if "model" in ckpt and isinstance(ckpt["model"], dict):
ckpt = ckpt["model"]
sam3_image_ckpt = {k.replace("detector.", ""): v for k, v in ckpt.items() if "detector" in k}
if interactive:
sam3_image_ckpt.update(
{
k.replace("backbone.vision_backbone", "image_encoder.vision_backbone"): v
for k, v in sam3_image_ckpt.items()
if "backbone.vision_backbone" in k
}
)
sam3_image_ckpt.update(
{
k.replace("tracker.transformer.encoder", "memory_attention"): v
for k, v in ckpt.items()
if "tracker.transformer" in k
}
)
sam3_image_ckpt.update(
{
k.replace("tracker.maskmem_backbone", "memory_encoder"): v
for k, v in ckpt.items()
if "tracker.maskmem_backbone" in k
}
)
sam3_image_ckpt.update({k.replace("tracker.", ""): v for k, v in ckpt.items() if "tracker." in k})
model.load_state_dict(sam3_image_ckpt, strict=False)
return model

View File

@@ -0,0 +1,169 @@
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
"""
SAM model interface.
This module provides an interface to the Segment Anything Model (SAM) from Ultralytics, designed for real-time image
segmentation tasks. The SAM model allows for promptable segmentation with unparalleled versatility in image analysis,
and has been trained on the SA-1B dataset. It features zero-shot performance capabilities, enabling it to adapt to new
image distributions and tasks without prior knowledge.
Key Features:
- Promptable segmentation
- Real-time performance
- Zero-shot transfer capabilities
- Trained on SA-1B dataset
"""
from __future__ import annotations
from pathlib import Path
from ultralytics.engine.model import Model
from ultralytics.utils.torch_utils import model_info
from .predict import Predictor, SAM2Predictor, SAM3Predictor
class SAM(Model):
"""SAM (Segment Anything Model) interface class for real-time image segmentation tasks.
This class provides an interface to the Segment Anything Model (SAM) from Ultralytics, designed for promptable
segmentation with versatility in image analysis. It supports various prompts such as bounding boxes, points, or
labels, and features zero-shot performance capabilities.
Attributes:
model (torch.nn.Module): The loaded SAM model.
is_sam2 (bool): Indicates whether the model is SAM2 variant.
task (str): The task type, set to "segment" for SAM models.
Methods:
predict: Perform segmentation prediction on the given image or video source.
info: Log information about the SAM model.
Examples:
>>> sam = SAM("sam_b.pt")
>>> results = sam.predict("image.jpg", points=[[500, 375]])
>>> for r in results:
... print(f"Detected {len(r.masks)} masks")
"""
def __init__(self, model: str = "sam_b.pt") -> None:
"""Initialize the SAM (Segment Anything Model) instance.
Args:
model (str): Path to the pre-trained SAM model file. File should have a .pt or .pth extension.
Raises:
NotImplementedError: If the model file extension is not .pt or .pth.
"""
if model and Path(model).suffix not in {".pt", ".pth"}:
raise NotImplementedError("SAM prediction requires pre-trained *.pt or *.pth model.")
self.is_sam2 = "sam2" in Path(model).stem
self.is_sam3 = "sam3" in Path(model).stem
super().__init__(model=model, task="segment")
def _load(self, weights: str, task=None):
"""Load the specified weights into the SAM model.
Args:
weights (str): Path to the weights file. Should be a .pt or .pth file containing the model parameters.
task (str | None): Task name. If provided, it specifies the particular task the model is being loaded for.
Examples:
>>> sam = SAM("sam_b.pt")
>>> sam._load("path/to/custom_weights.pt")
"""
if self.is_sam3:
from .build_sam3 import build_interactive_sam3
self.model = build_interactive_sam3(weights)
else:
from .build import build_sam # slow import
self.model = build_sam(weights)
def predict(self, source, stream: bool = False, bboxes=None, points=None, labels=None, **kwargs):
"""Perform segmentation prediction on the given image or video source.
Args:
source (str | PIL.Image | np.ndarray): Path to the image or video file, or a PIL.Image object, or a
np.ndarray object.
stream (bool): If True, enables real-time streaming.
bboxes (list[list[float]] | None): List of bounding box coordinates for prompted segmentation.
points (list[list[float]] | None): List of points for prompted segmentation.
labels (list[int] | None): List of labels for prompted segmentation.
**kwargs (Any): Additional keyword arguments for prediction.
Returns:
(list): The model predictions.
Examples:
>>> sam = SAM("sam_b.pt")
>>> results = sam.predict("image.jpg", points=[[500, 375]])
>>> for r in results:
... print(f"Detected {len(r.masks)} masks")
"""
overrides = dict(conf=0.25, task="segment", mode="predict", imgsz=1024)
kwargs = {**overrides, **kwargs}
prompts = dict(bboxes=bboxes, points=points, labels=labels)
return super().predict(source, stream, prompts=prompts, **kwargs)
def __call__(self, source=None, stream: bool = False, bboxes=None, points=None, labels=None, **kwargs):
"""Perform segmentation prediction on the given image or video source.
This method is an alias for the 'predict' method, providing a convenient way to call the SAM model for
segmentation tasks.
Args:
source (str | PIL.Image | np.ndarray | None): Path to the image or video file, or a PIL.Image object, or a
np.ndarray object.
stream (bool): If True, enables real-time streaming.
bboxes (list[list[float]] | None): List of bounding box coordinates for prompted segmentation.
points (list[list[float]] | None): List of points for prompted segmentation.
labels (list[int] | None): List of labels for prompted segmentation.
**kwargs (Any): Additional keyword arguments to be passed to the predict method.
Returns:
(list): The model predictions, typically containing segmentation masks and other relevant information.
Examples:
>>> sam = SAM("sam_b.pt")
>>> results = sam("image.jpg", points=[[500, 375]])
>>> print(f"Detected {len(results[0].masks)} masks")
"""
return self.predict(source, stream, bboxes, points, labels, **kwargs)
def info(self, detailed: bool = False, verbose: bool = True):
"""Log information about the SAM model.
Args:
detailed (bool): If True, displays detailed information about the model layers and operations.
verbose (bool): If True, prints the information to the console.
Returns:
(tuple): A tuple containing the model's information (string representations of the model).
Examples:
>>> sam = SAM("sam_b.pt")
>>> info = sam.info()
>>> print(info[0]) # Print summary information
"""
return model_info(self.model, detailed=detailed, verbose=verbose)
@property
def task_map(self) -> dict[str, dict[str, type[Predictor]]]:
"""Provide a mapping from the 'segment' task to its corresponding 'Predictor'.
Returns:
(dict[str, dict[str, Type[Predictor]]]): A dictionary mapping the 'segment' task to its corresponding
Predictor class. For SAM2 models, it maps to SAM2Predictor, otherwise to the standard Predictor.
Examples:
>>> sam = SAM("sam_b.pt")
>>> task_map = sam.task_map
>>> print(task_map)
{'segment': {'predictor': <class 'ultralytics.models.sam.predict.Predictor'>}}
"""
return {
"segment": {"predictor": SAM2Predictor if self.is_sam2 else SAM3Predictor if self.is_sam3 else Predictor}
}

View File

@@ -0,0 +1 @@
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,495 @@
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
from __future__ import annotations
import torch
from torch import nn
from ultralytics.nn.modules import MLP, LayerNorm2d
class MaskDecoder(nn.Module):
"""Decoder module for generating masks and their associated quality scores using a transformer architecture.
This class predicts masks given image and prompt embeddings, utilizing a transformer to process the inputs and
generate mask predictions along with their quality scores.
Attributes:
transformer_dim (int): Channel dimension for the transformer module.
transformer (nn.Module): Transformer module used for mask prediction.
num_multimask_outputs (int): Number of masks to predict for disambiguating masks.
iou_token (nn.Embedding): Embedding for the IoU token.
num_mask_tokens (int): Number of mask tokens.
mask_tokens (nn.Embedding): Embedding for the mask tokens.
output_upscaling (nn.Sequential): Neural network sequence for upscaling the output.
output_hypernetworks_mlps (nn.ModuleList): Hypernetwork MLPs for generating masks.
iou_prediction_head (nn.Module): MLP for predicting mask quality.
Methods:
forward: Predict masks given image and prompt embeddings.
predict_masks: Internal method for mask prediction.
Examples:
>>> decoder = MaskDecoder(transformer_dim=256, transformer=transformer_module)
>>> masks, iou_pred = decoder(
... image_embeddings, image_pe, sparse_prompt_embeddings, dense_prompt_embeddings, multimask_output=True
... )
>>> print(f"Predicted masks shape: {masks.shape}, IoU predictions shape: {iou_pred.shape}")
"""
def __init__(
self,
transformer_dim: int,
transformer: nn.Module,
num_multimask_outputs: int = 3,
activation: type[nn.Module] = nn.GELU,
iou_head_depth: int = 3,
iou_head_hidden_dim: int = 256,
) -> None:
"""Initialize the MaskDecoder module for generating masks and their associated quality scores.
Args:
transformer_dim (int): Channel dimension for the transformer module.
transformer (nn.Module): Transformer module used for mask prediction.
num_multimask_outputs (int): Number of masks to predict for disambiguating masks.
activation (Type[nn.Module]): Type of activation to use when upscaling masks.
iou_head_depth (int): Depth of the MLP used to predict mask quality.
iou_head_hidden_dim (int): Hidden dimension of the MLP used to predict mask quality.
"""
super().__init__()
self.transformer_dim = transformer_dim
self.transformer = transformer
self.num_multimask_outputs = num_multimask_outputs
self.iou_token = nn.Embedding(1, transformer_dim)
self.num_mask_tokens = num_multimask_outputs + 1
self.mask_tokens = nn.Embedding(self.num_mask_tokens, transformer_dim)
self.output_upscaling = nn.Sequential(
nn.ConvTranspose2d(transformer_dim, transformer_dim // 4, kernel_size=2, stride=2),
LayerNorm2d(transformer_dim // 4),
activation(),
nn.ConvTranspose2d(transformer_dim // 4, transformer_dim // 8, kernel_size=2, stride=2),
activation(),
)
self.output_hypernetworks_mlps = nn.ModuleList(
[MLP(transformer_dim, transformer_dim, transformer_dim // 8, 3) for _ in range(self.num_mask_tokens)]
)
self.iou_prediction_head = MLP(transformer_dim, iou_head_hidden_dim, self.num_mask_tokens, iou_head_depth)
def forward(
self,
image_embeddings: torch.Tensor,
image_pe: torch.Tensor,
sparse_prompt_embeddings: torch.Tensor,
dense_prompt_embeddings: torch.Tensor,
multimask_output: bool,
) -> tuple[torch.Tensor, torch.Tensor]:
"""Predict masks given image and prompt embeddings.
Args:
image_embeddings (torch.Tensor): Embeddings from the image encoder.
image_pe (torch.Tensor): Positional encoding with the shape of image_embeddings.
sparse_prompt_embeddings (torch.Tensor): Embeddings of the points and boxes.
dense_prompt_embeddings (torch.Tensor): Embeddings of the mask inputs.
multimask_output (bool): Whether to return multiple masks or a single mask.
Returns:
masks (torch.Tensor): Batched predicted masks.
iou_pred (torch.Tensor): Batched predictions of mask quality.
Examples:
>>> decoder = MaskDecoder(transformer_dim=256, transformer=transformer_module)
>>> image_emb = torch.rand(1, 256, 64, 64)
>>> image_pe = torch.rand(1, 256, 64, 64)
>>> sparse_emb = torch.rand(1, 2, 256)
>>> dense_emb = torch.rand(1, 256, 64, 64)
>>> masks, iou_pred = decoder(image_emb, image_pe, sparse_emb, dense_emb, multimask_output=True)
>>> print(f"Masks shape: {masks.shape}, IoU predictions shape: {iou_pred.shape}")
"""
masks, iou_pred = self.predict_masks(
image_embeddings=image_embeddings,
image_pe=image_pe,
sparse_prompt_embeddings=sparse_prompt_embeddings,
dense_prompt_embeddings=dense_prompt_embeddings,
)
# Select the correct mask or masks for output
mask_slice = slice(1, None) if multimask_output else slice(0, 1)
masks = masks[:, mask_slice, :, :]
iou_pred = iou_pred[:, mask_slice]
return masks, iou_pred
def predict_masks(
self,
image_embeddings: torch.Tensor,
image_pe: torch.Tensor,
sparse_prompt_embeddings: torch.Tensor,
dense_prompt_embeddings: torch.Tensor,
) -> tuple[torch.Tensor, torch.Tensor]:
"""Predict masks and quality scores using image and prompt embeddings via transformer architecture."""
# Concatenate output tokens
output_tokens = torch.cat([self.iou_token.weight, self.mask_tokens.weight], dim=0)
output_tokens = output_tokens.unsqueeze(0).expand(sparse_prompt_embeddings.shape[0], -1, -1)
tokens = torch.cat((output_tokens, sparse_prompt_embeddings), dim=1)
# Expand per-image data in batch direction to be per-mask
src = torch.repeat_interleave(image_embeddings, tokens.shape[0], dim=0)
src = src + dense_prompt_embeddings
pos_src = torch.repeat_interleave(image_pe, tokens.shape[0], dim=0)
b, c, h, w = src.shape
# Run the transformer
hs, src = self.transformer(src, pos_src, tokens)
iou_token_out = hs[:, 0, :]
mask_tokens_out = hs[:, 1 : (1 + self.num_mask_tokens), :]
# Upscale mask embeddings and predict masks using the mask tokens
src = src.transpose(1, 2).view(b, c, h, w)
upscaled_embedding = self.output_upscaling(src)
hyper_in_list: list[torch.Tensor] = [
self.output_hypernetworks_mlps[i](mask_tokens_out[:, i, :]) for i in range(self.num_mask_tokens)
]
hyper_in = torch.stack(hyper_in_list, dim=1)
b, c, h, w = upscaled_embedding.shape
masks = (hyper_in @ upscaled_embedding.view(b, c, h * w)).view(b, -1, h, w)
# Generate mask quality predictions
iou_pred = self.iou_prediction_head(iou_token_out)
return masks, iou_pred
class SAM2MaskDecoder(nn.Module):
"""Transformer-based decoder for predicting instance segmentation masks from image and prompt embeddings.
This class extends the functionality of the MaskDecoder, incorporating additional features such as high-resolution
feature processing, dynamic multimask output, and object score prediction.
Attributes:
transformer_dim (int): Channel dimension of the transformer.
transformer (nn.Module): Transformer used to predict masks.
num_multimask_outputs (int): Number of masks to predict when disambiguating masks.
iou_token (nn.Embedding): Embedding for IOU token.
num_mask_tokens (int): Total number of mask tokens.
mask_tokens (nn.Embedding): Embedding for mask tokens.
pred_obj_scores (bool): Whether to predict object scores.
obj_score_token (nn.Embedding): Embedding for object score token.
use_multimask_token_for_obj_ptr (bool): Whether to use multimask token for object pointer.
output_upscaling (nn.Sequential): Upscaling layers for output.
use_high_res_features (bool): Whether to use high-resolution features.
conv_s0 (nn.Conv2d): Convolutional layer for high-resolution features (s0).
conv_s1 (nn.Conv2d): Convolutional layer for high-resolution features (s1).
output_hypernetworks_mlps (nn.ModuleList): List of MLPs for output hypernetworks.
iou_prediction_head (MLP): MLP for IOU prediction.
pred_obj_score_head (nn.Linear | MLP): Linear layer or MLP for object score prediction.
dynamic_multimask_via_stability (bool): Whether to use dynamic multimask via stability.
dynamic_multimask_stability_delta (float): Delta value for dynamic multimask stability.
dynamic_multimask_stability_thresh (float): Threshold for dynamic multimask stability.
Methods:
forward: Predict masks given image and prompt embeddings.
predict_masks: Predict instance segmentation masks from image and prompt embeddings.
_get_stability_scores: Compute mask stability scores based on IoU between thresholds.
_dynamic_multimask_via_stability: Dynamically select the most stable mask output.
Examples:
>>> image_embeddings = torch.rand(1, 256, 64, 64)
>>> image_pe = torch.rand(1, 256, 64, 64)
>>> sparse_prompt_embeddings = torch.rand(1, 2, 256)
>>> dense_prompt_embeddings = torch.rand(1, 256, 64, 64)
>>> decoder = SAM2MaskDecoder(256, transformer)
>>> masks, iou_pred, sam_tokens_out, obj_score_logits = decoder.forward(
... image_embeddings, image_pe, sparse_prompt_embeddings, dense_prompt_embeddings, True, False
... )
"""
def __init__(
self,
transformer_dim: int,
transformer: nn.Module,
num_multimask_outputs: int = 3,
activation: type[nn.Module] = nn.GELU,
iou_head_depth: int = 3,
iou_head_hidden_dim: int = 256,
use_high_res_features: bool = False,
iou_prediction_use_sigmoid=False,
dynamic_multimask_via_stability=False,
dynamic_multimask_stability_delta=0.05,
dynamic_multimask_stability_thresh=0.98,
pred_obj_scores: bool = False,
pred_obj_scores_mlp: bool = False,
use_multimask_token_for_obj_ptr: bool = False,
) -> None:
"""Initialize the SAM2MaskDecoder module for predicting instance segmentation masks.
This decoder extends the functionality of MaskDecoder, incorporating additional features such as high-resolution
feature processing, dynamic multimask output, and object score prediction.
Args:
transformer_dim (int): Channel dimension of the transformer.
transformer (nn.Module): Transformer used to predict masks.
num_multimask_outputs (int): Number of masks to predict when disambiguating masks.
activation (Type[nn.Module]): Type of activation to use when upscaling masks.
iou_head_depth (int): Depth of the MLP used to predict mask quality.
iou_head_hidden_dim (int): Hidden dimension of the MLP used to predict mask quality.
use_high_res_features (bool): Whether to use high-resolution features.
iou_prediction_use_sigmoid (bool): Whether to use sigmoid for IOU prediction.
dynamic_multimask_via_stability (bool): Whether to use dynamic multimask via stability.
dynamic_multimask_stability_delta (float): Delta value for dynamic multimask stability.
dynamic_multimask_stability_thresh (float): Threshold for dynamic multimask stability.
pred_obj_scores (bool): Whether to predict object scores.
pred_obj_scores_mlp (bool): Whether to use MLP for object score prediction.
use_multimask_token_for_obj_ptr (bool): Whether to use multimask token for object pointer.
"""
super().__init__()
self.transformer_dim = transformer_dim
self.transformer = transformer
self.num_multimask_outputs = num_multimask_outputs
self.iou_token = nn.Embedding(1, transformer_dim)
self.num_mask_tokens = num_multimask_outputs + 1
self.mask_tokens = nn.Embedding(self.num_mask_tokens, transformer_dim)
self.pred_obj_scores = pred_obj_scores
if self.pred_obj_scores:
self.obj_score_token = nn.Embedding(1, transformer_dim)
self.use_multimask_token_for_obj_ptr = use_multimask_token_for_obj_ptr
self.output_upscaling = nn.Sequential(
nn.ConvTranspose2d(transformer_dim, transformer_dim // 4, kernel_size=2, stride=2),
LayerNorm2d(transformer_dim // 4),
activation(),
nn.ConvTranspose2d(transformer_dim // 4, transformer_dim // 8, kernel_size=2, stride=2),
activation(),
)
self.use_high_res_features = use_high_res_features
if use_high_res_features:
self.conv_s0 = nn.Conv2d(transformer_dim, transformer_dim // 8, kernel_size=1, stride=1)
self.conv_s1 = nn.Conv2d(transformer_dim, transformer_dim // 4, kernel_size=1, stride=1)
self.output_hypernetworks_mlps = nn.ModuleList(
[MLP(transformer_dim, transformer_dim, transformer_dim // 8, 3) for _ in range(self.num_mask_tokens)]
)
self.iou_prediction_head = MLP(
transformer_dim,
iou_head_hidden_dim,
self.num_mask_tokens,
iou_head_depth,
sigmoid=iou_prediction_use_sigmoid,
)
if self.pred_obj_scores:
self.pred_obj_score_head = nn.Linear(transformer_dim, 1)
if pred_obj_scores_mlp:
self.pred_obj_score_head = MLP(transformer_dim, transformer_dim, 1, 3)
# When outputting a single mask, optionally we can dynamically fall back to the best
# multimask output token if the single mask output token gives low stability scores.
self.dynamic_multimask_via_stability = dynamic_multimask_via_stability
self.dynamic_multimask_stability_delta = dynamic_multimask_stability_delta
self.dynamic_multimask_stability_thresh = dynamic_multimask_stability_thresh
def forward(
self,
image_embeddings: torch.Tensor,
image_pe: torch.Tensor,
sparse_prompt_embeddings: torch.Tensor,
dense_prompt_embeddings: torch.Tensor,
multimask_output: bool,
repeat_image: bool,
high_res_features: list[torch.Tensor] | None = None,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
"""Predict masks given image and prompt embeddings.
Args:
image_embeddings (torch.Tensor): Embeddings from the image encoder with shape (B, C, H, W).
image_pe (torch.Tensor): Positional encoding with the shape of image_embeddings (B, C, H, W).
sparse_prompt_embeddings (torch.Tensor): Embeddings of the points and boxes with shape (B, N, C).
dense_prompt_embeddings (torch.Tensor): Embeddings of the mask inputs with shape (B, C, H, W).
multimask_output (bool): Whether to return multiple masks or a single mask.
repeat_image (bool): Flag to repeat the image embeddings.
high_res_features (list[torch.Tensor] | None, optional): Optional high-resolution features.
Returns:
masks (torch.Tensor): Batched predicted masks with shape (B, N, H, W).
iou_pred (torch.Tensor): Batched predictions of mask quality with shape (B, N).
sam_tokens_out (torch.Tensor): Batched SAM token for mask output with shape (B, N, C).
object_score_logits (torch.Tensor): Batched object score logits with shape (B, 1).
Examples:
>>> image_embeddings = torch.rand(1, 256, 64, 64)
>>> image_pe = torch.rand(1, 256, 64, 64)
>>> sparse_prompt_embeddings = torch.rand(1, 2, 256)
>>> dense_prompt_embeddings = torch.rand(1, 256, 64, 64)
>>> decoder = SAM2MaskDecoder(256, transformer)
>>> masks, iou_pred, sam_tokens_out, obj_score_logits = decoder.forward(
... image_embeddings, image_pe, sparse_prompt_embeddings, dense_prompt_embeddings, True, False
... )
"""
masks, iou_pred, mask_tokens_out, object_score_logits = self.predict_masks(
image_embeddings=image_embeddings,
image_pe=image_pe,
sparse_prompt_embeddings=sparse_prompt_embeddings,
dense_prompt_embeddings=dense_prompt_embeddings,
repeat_image=repeat_image,
high_res_features=high_res_features,
)
# Select the correct mask or masks for output
if multimask_output:
masks = masks[:, 1:, :, :]
iou_pred = iou_pred[:, 1:]
elif self.dynamic_multimask_via_stability and not self.training:
masks, iou_pred = self._dynamic_multimask_via_stability(masks, iou_pred)
else:
masks = masks[:, 0:1, :, :]
iou_pred = iou_pred[:, 0:1]
if multimask_output and self.use_multimask_token_for_obj_ptr:
sam_tokens_out = mask_tokens_out[:, 1:] # [b, 3, c] shape
else:
# Take the mask output token. Here we *always* use the token for single mask output.
# At test time, even if we track after 1-click (and using multimask_output=True),
# we still take the single mask token here. The rationale is that we always track
# after multiple clicks during training, so the past tokens seen during training
# are always the single mask token (and we'll let it be the object-memory token).
sam_tokens_out = mask_tokens_out[:, 0:1] # [b, 1, c] shape
return masks, iou_pred, sam_tokens_out, object_score_logits
def predict_masks(
self,
image_embeddings: torch.Tensor,
image_pe: torch.Tensor,
sparse_prompt_embeddings: torch.Tensor,
dense_prompt_embeddings: torch.Tensor,
repeat_image: bool,
high_res_features: list[torch.Tensor] | None = None,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
"""Predict instance segmentation masks from image and prompt embeddings using a transformer."""
# Concatenate output tokens
s = 0
if self.pred_obj_scores:
output_tokens = torch.cat(
[
self.obj_score_token.weight,
self.iou_token.weight,
self.mask_tokens.weight,
],
dim=0,
)
s = 1
else:
output_tokens = torch.cat([self.iou_token.weight, self.mask_tokens.weight], dim=0)
output_tokens = output_tokens.unsqueeze(0).expand(sparse_prompt_embeddings.shape[0], -1, -1)
tokens = torch.cat((output_tokens, sparse_prompt_embeddings), dim=1)
# Expand per-image data in batch direction to be per-mask
if repeat_image:
src = torch.repeat_interleave(image_embeddings, tokens.shape[0], dim=0)
else:
assert image_embeddings.shape[0] == tokens.shape[0]
src = image_embeddings
src = src + dense_prompt_embeddings
assert image_pe.shape[0] == 1, "image_pe should have size 1 in batch dim (from `get_dense_pe()`)"
pos_src = torch.repeat_interleave(image_pe, tokens.shape[0], dim=0)
b, c, h, w = src.shape
# Run the transformer
hs, src = self.transformer(src, pos_src, tokens)
iou_token_out = hs[:, s, :]
mask_tokens_out = hs[:, s + 1 : (s + 1 + self.num_mask_tokens), :]
# Upscale mask embeddings and predict masks using the mask tokens
src = src.transpose(1, 2).view(b, c, h, w)
if not self.use_high_res_features or high_res_features is None:
upscaled_embedding = self.output_upscaling(src)
else:
dc1, ln1, act1, dc2, act2 = self.output_upscaling
feat_s0, feat_s1 = high_res_features
upscaled_embedding = act1(ln1(dc1(src) + feat_s1))
upscaled_embedding = act2(dc2(upscaled_embedding) + feat_s0)
hyper_in_list: list[torch.Tensor] = [
self.output_hypernetworks_mlps[i](mask_tokens_out[:, i, :]) for i in range(self.num_mask_tokens)
]
hyper_in = torch.stack(hyper_in_list, dim=1)
b, c, h, w = upscaled_embedding.shape
masks = (hyper_in @ upscaled_embedding.view(b, c, h * w)).view(b, -1, h, w)
# Generate mask quality predictions
iou_pred = self.iou_prediction_head(iou_token_out)
if self.pred_obj_scores:
assert s == 1
object_score_logits = self.pred_obj_score_head(hs[:, 0, :])
else:
# Obj scores logits - default to 10.0, i.e. assuming the object is present, sigmoid(10)=1
object_score_logits = 10.0 * iou_pred.new_ones(iou_pred.shape[0], 1)
return masks, iou_pred, mask_tokens_out, object_score_logits
def _get_stability_scores(self, mask_logits):
"""Compute mask stability scores based on IoU between upper and lower thresholds."""
mask_logits = mask_logits.flatten(-2)
area_i = torch.sum(mask_logits > self.dynamic_multimask_stability_delta, dim=-1).float()
area_u = torch.sum(mask_logits > -self.dynamic_multimask_stability_delta, dim=-1).float()
return torch.where(area_u > 0, area_i / area_u, 1.0)
def _dynamic_multimask_via_stability(self, all_mask_logits, all_iou_scores):
"""Dynamically select the most stable mask output based on stability scores and IoU predictions.
This method is used when outputting a single mask. If the stability score from the current single-mask output
(based on output token 0) falls below a threshold, it instead selects from multi-mask outputs (based on output
tokens 1-3) the mask with the highest predicted IoU score. This ensures a valid mask for both clicking and
tracking scenarios.
Args:
all_mask_logits (torch.Tensor): Logits for all predicted masks, shape (B, N, H, W) where B is batch size, N
is number of masks (typically 4), and H, W are mask dimensions.
all_iou_scores (torch.Tensor): Predicted IoU scores for all masks, shape (B, N).
Returns:
mask_logits_out (torch.Tensor): Selected mask logits, shape (B, 1, H, W).
iou_scores_out (torch.Tensor): Selected IoU scores, shape (B, 1).
Examples:
>>> decoder = SAM2MaskDecoder(...)
>>> all_mask_logits = torch.rand(2, 4, 256, 256) # 2 images, 4 masks each
>>> all_iou_scores = torch.rand(2, 4)
>>> mask_logits, iou_scores = decoder._dynamic_multimask_via_stability(all_mask_logits, all_iou_scores)
>>> print(mask_logits.shape, iou_scores.shape)
torch.Size([2, 1, 256, 256]) torch.Size([2, 1])
"""
# The best mask from multimask output tokens (1~3)
multimask_logits = all_mask_logits[:, 1:, :, :]
multimask_iou_scores = all_iou_scores[:, 1:]
best_scores_inds = torch.argmax(multimask_iou_scores, dim=-1)
batch_inds = torch.arange(multimask_iou_scores.shape[0], device=all_iou_scores.device)
best_multimask_logits = multimask_logits[batch_inds, best_scores_inds]
best_multimask_logits = best_multimask_logits.unsqueeze(1)
best_multimask_iou_scores = multimask_iou_scores[batch_inds, best_scores_inds]
best_multimask_iou_scores = best_multimask_iou_scores.unsqueeze(1)
# The mask from singlemask output token 0 and its stability score
singlemask_logits = all_mask_logits[:, 0:1, :, :]
singlemask_iou_scores = all_iou_scores[:, 0:1]
stability_scores = self._get_stability_scores(singlemask_logits)
is_stable = stability_scores >= self.dynamic_multimask_stability_thresh
# Dynamically fall back to best multimask output upon low stability scores.
mask_logits_out = torch.where(
is_stable[..., None, None].expand_as(singlemask_logits),
singlemask_logits,
best_multimask_logits,
)
iou_scores_out = torch.where(
is_stable.expand_as(singlemask_iou_scores),
singlemask_iou_scores,
best_multimask_iou_scores,
)
return mask_logits_out, iou_scores_out

View File

@@ -0,0 +1,794 @@
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
from __future__ import annotations
import torch
import torch.nn as nn
import torch.nn.functional as F
from ultralytics.nn.modules import LayerNorm2d
from .blocks import (
Block,
CXBlock,
Fuser,
MaskDownSampler,
MultiScaleBlock,
PatchEmbed,
PositionEmbeddingRandom,
PositionEmbeddingSine,
)
class ImageEncoderViT(nn.Module):
"""An image encoder using Vision Transformer (ViT) architecture for encoding images into a compact latent space.
This class processes images by splitting them into patches, applying transformer blocks, and generating a final
encoded representation through a neck module.
Attributes:
img_size (int): Dimension of input images, assumed to be square.
patch_embed (PatchEmbed): Module for patch embedding.
pos_embed (nn.Parameter | None): Absolute positional embedding for patches.
blocks (nn.ModuleList): List of transformer blocks for processing patch embeddings.
neck (nn.Sequential): Neck module to further process the output.
Methods:
forward: Process input through patch embedding, positional embedding, blocks, and neck.
Examples:
>>> import torch
>>> encoder = ImageEncoderViT(img_size=224, patch_size=16, embed_dim=768, depth=12, num_heads=12)
>>> input_image = torch.randn(1, 3, 224, 224)
>>> output = encoder(input_image)
>>> print(output.shape)
"""
def __init__(
self,
img_size: int = 1024,
patch_size: int = 16,
in_chans: int = 3,
embed_dim: int = 768,
depth: int = 12,
num_heads: int = 12,
mlp_ratio: float = 4.0,
out_chans: int = 256,
qkv_bias: bool = True,
norm_layer: type[nn.Module] = nn.LayerNorm,
act_layer: type[nn.Module] = nn.GELU,
use_abs_pos: bool = True,
use_rel_pos: bool = False,
rel_pos_zero_init: bool = True,
window_size: int = 0,
global_attn_indexes: tuple[int, ...] = (),
) -> None:
"""Initialize an ImageEncoderViT instance for encoding images using Vision Transformer architecture.
Args:
img_size (int): Input image size, assumed to be square.
patch_size (int): Size of image patches.
in_chans (int): Number of input image channels.
embed_dim (int): Dimension of patch embeddings.
depth (int): Number of transformer blocks.
num_heads (int): Number of attention heads in each block.
mlp_ratio (float): Ratio of MLP hidden dimension to embedding dimension.
out_chans (int): Number of output channels from the neck module.
qkv_bias (bool): If True, adds learnable bias to query, key, value projections.
norm_layer (Type[nn.Module]): Type of normalization layer to use.
act_layer (Type[nn.Module]): Type of activation layer to use.
use_abs_pos (bool): If True, uses absolute positional embeddings.
use_rel_pos (bool): If True, adds relative positional embeddings to attention maps.
rel_pos_zero_init (bool): If True, initializes relative positional parameters to zero.
window_size (int): Size of attention window for windowed attention blocks.
global_attn_indexes (tuple[int, ...]): Indices of blocks that use global attention.
"""
super().__init__()
self.img_size = img_size
self.patch_embed = PatchEmbed(
kernel_size=(patch_size, patch_size),
stride=(patch_size, patch_size),
in_chans=in_chans,
embed_dim=embed_dim,
)
self.pos_embed: nn.Parameter | None = None
if use_abs_pos:
# Initialize absolute positional embedding with pretrain image size
self.pos_embed = nn.Parameter(torch.zeros(1, img_size // patch_size, img_size // patch_size, embed_dim))
self.blocks = nn.ModuleList()
for i in range(depth):
block = Block(
dim=embed_dim,
num_heads=num_heads,
mlp_ratio=mlp_ratio,
qkv_bias=qkv_bias,
norm_layer=norm_layer,
act_layer=act_layer,
use_rel_pos=use_rel_pos,
rel_pos_zero_init=rel_pos_zero_init,
window_size=window_size if i not in global_attn_indexes else 0,
input_size=(img_size // patch_size, img_size // patch_size),
)
self.blocks.append(block)
self.neck = nn.Sequential(
nn.Conv2d(
embed_dim,
out_chans,
kernel_size=1,
bias=False,
),
LayerNorm2d(out_chans),
nn.Conv2d(
out_chans,
out_chans,
kernel_size=3,
padding=1,
bias=False,
),
LayerNorm2d(out_chans),
)
def forward(self, x: torch.Tensor) -> torch.Tensor:
"""Process input through patch embedding, positional embedding, transformer blocks, and neck module."""
x = self.patch_embed(x)
if self.pos_embed is not None:
pos_embed = (
F.interpolate(self.pos_embed.permute(0, 3, 1, 2), scale_factor=self.img_size / 1024).permute(0, 2, 3, 1)
if self.img_size != 1024
else self.pos_embed
)
x = x + pos_embed
for blk in self.blocks:
x = blk(x)
return self.neck(x.permute(0, 3, 1, 2))
class PromptEncoder(nn.Module):
"""Encode different types of prompts for input to SAM's mask decoder, producing sparse and dense embeddings.
Attributes:
embed_dim (int): Dimension of the embeddings.
input_image_size (tuple[int, int]): Size of the input image as (H, W).
image_embedding_size (tuple[int, int]): Spatial size of the image embedding as (H, W).
pe_layer (PositionEmbeddingRandom): Module for random position embedding.
num_point_embeddings (int): Number of point embeddings for different types of points.
point_embeddings (nn.ModuleList): List of point embeddings.
not_a_point_embed (nn.Embedding): Embedding for points that are not part of any label.
mask_input_size (tuple[int, int]): Size of the input mask.
mask_downscaling (nn.Sequential): Neural network for downscaling the mask.
no_mask_embed (nn.Embedding): Embedding for cases where no mask is provided.
Methods:
get_dense_pe: Return the positional encoding used to encode point prompts.
forward: Embed different types of prompts, returning both sparse and dense embeddings.
Examples:
>>> prompt_encoder = PromptEncoder(256, (64, 64), (1024, 1024), 16)
>>> points = (torch.rand(1, 5, 2), torch.randint(0, 4, (1, 5)))
>>> boxes = torch.rand(1, 2, 2)
>>> masks = torch.rand(1, 1, 256, 256)
>>> sparse_embeddings, dense_embeddings = prompt_encoder(points, boxes, masks)
>>> print(sparse_embeddings.shape, dense_embeddings.shape)
torch.Size([1, 7, 256]) torch.Size([1, 256, 64, 64])
"""
def __init__(
self,
embed_dim: int,
image_embedding_size: tuple[int, int],
input_image_size: tuple[int, int],
mask_in_chans: int,
activation: type[nn.Module] = nn.GELU,
) -> None:
"""Initialize the PromptEncoder module for encoding various types of prompts.
Args:
embed_dim (int): The dimension of the embeddings.
image_embedding_size (tuple[int, int]): The spatial size of the image embedding as (H, W).
input_image_size (tuple[int, int]): The padded size of the input image as (H, W).
mask_in_chans (int): The number of hidden channels used for encoding input masks.
activation (Type[nn.Module]): The activation function to use when encoding input masks.
"""
super().__init__()
self.embed_dim = embed_dim
self.input_image_size = input_image_size
self.image_embedding_size = image_embedding_size
self.pe_layer = PositionEmbeddingRandom(embed_dim // 2)
self.num_point_embeddings: int = 4 # pos/neg point + 2 box corners
point_embeddings = [nn.Embedding(1, embed_dim) for _ in range(self.num_point_embeddings)]
self.point_embeddings = nn.ModuleList(point_embeddings)
self.not_a_point_embed = nn.Embedding(1, embed_dim)
self.mask_input_size = (4 * image_embedding_size[0], 4 * image_embedding_size[1])
self.mask_downscaling = nn.Sequential(
nn.Conv2d(1, mask_in_chans // 4, kernel_size=2, stride=2),
LayerNorm2d(mask_in_chans // 4),
activation(),
nn.Conv2d(mask_in_chans // 4, mask_in_chans, kernel_size=2, stride=2),
LayerNorm2d(mask_in_chans),
activation(),
nn.Conv2d(mask_in_chans, embed_dim, kernel_size=1),
)
self.no_mask_embed = nn.Embedding(1, embed_dim)
def get_dense_pe(self) -> torch.Tensor:
"""Return the dense positional encoding used for encoding point prompts.
Generate a positional encoding for a dense set of points matching the shape of the image
encoding. The encoding is used to provide spatial information to the model when processing point prompts.
Returns:
(torch.Tensor): Positional encoding tensor with shape (1, embed_dim, H, W), where H and W are the height and
width of the image embedding size, respectively.
Examples:
>>> prompt_encoder = PromptEncoder(256, (64, 64), (1024, 1024), 16)
>>> dense_pe = prompt_encoder.get_dense_pe()
>>> print(dense_pe.shape)
torch.Size([1, 256, 64, 64])
"""
return self.pe_layer(self.image_embedding_size).unsqueeze(0)
def _embed_points(self, points: torch.Tensor, labels: torch.Tensor, pad: bool) -> torch.Tensor:
"""Embed point prompts by applying positional encoding and label-specific embeddings."""
points = points + 0.5 # Shift to center of pixel
if pad:
padding_point = torch.zeros((points.shape[0], 1, 2), dtype=points.dtype, device=points.device)
padding_label = -torch.ones((labels.shape[0], 1), dtype=labels.dtype, device=labels.device)
points = torch.cat([points, padding_point], dim=1)
labels = torch.cat([labels, padding_label], dim=1)
point_embedding = self.pe_layer.forward_with_coords(points, self.input_image_size)
point_embedding[labels == -1] = 0.0
point_embedding[labels == -1] += self.not_a_point_embed.weight
point_embedding[labels == 0] += self.point_embeddings[0].weight
point_embedding[labels == 1] += self.point_embeddings[1].weight
point_embedding[labels == 2] += self.point_embeddings[2].weight
point_embedding[labels == 3] += self.point_embeddings[3].weight
return point_embedding
def _embed_boxes(self, boxes: torch.Tensor) -> torch.Tensor:
"""Embed box prompts by applying positional encoding and adding corner embeddings."""
boxes = boxes + 0.5 # Shift to center of pixel
coords = boxes.reshape(-1, 2, 2)
corner_embedding = self.pe_layer.forward_with_coords(coords, self.input_image_size)
corner_embedding[:, 0, :] += self.point_embeddings[2].weight
corner_embedding[:, 1, :] += self.point_embeddings[3].weight
return corner_embedding
def _embed_masks(self, masks: torch.Tensor) -> torch.Tensor:
"""Embed mask inputs by downscaling and processing through convolutional layers."""
return self.mask_downscaling(masks)
@staticmethod
def _get_batch_size(
points: tuple[torch.Tensor, torch.Tensor] | None,
boxes: torch.Tensor | None,
masks: torch.Tensor | None,
) -> int:
"""Get the batch size of the output given the batch size of the input prompts."""
if points is not None:
return points[0].shape[0]
elif boxes is not None:
return boxes.shape[0]
elif masks is not None:
return masks.shape[0]
else:
return 1
def forward(
self,
points: tuple[torch.Tensor, torch.Tensor] | None,
boxes: torch.Tensor | None,
masks: torch.Tensor | None,
) -> tuple[torch.Tensor, torch.Tensor]:
"""Embed different types of prompts, returning both sparse and dense embeddings.
Args:
points (tuple[torch.Tensor, torch.Tensor] | None): Point coordinates and labels to embed. The first tensor
contains coordinates of shape (B, N, 2), and the second tensor contains labels of shape (B, N).
boxes (torch.Tensor | None): Boxes to embed with shape (B, M, 2, 2), where M is the number of boxes.
masks (torch.Tensor | None): Masks to embed with shape (B, 1, H, W).
Returns:
sparse_embeddings (torch.Tensor): Sparse embeddings for points and boxes with shape (B, N, embed_dim).
dense_embeddings (torch.Tensor): Dense embeddings for masks of shape (B, embed_dim, embed_H, embed_W).
Examples:
>>> encoder = PromptEncoder(256, (64, 64), (1024, 1024), 16)
>>> points = (torch.rand(1, 5, 2), torch.randint(0, 4, (1, 5)))
>>> boxes = torch.rand(1, 2, 2, 2)
>>> masks = torch.rand(1, 1, 256, 256)
>>> sparse_emb, dense_emb = encoder(points, boxes, masks)
>>> print(sparse_emb.shape, dense_emb.shape)
torch.Size([1, 7, 256]) torch.Size([1, 256, 64, 64])
"""
bs = self._get_batch_size(points, boxes, masks)
sparse_embeddings = torch.empty(
(bs, 0, self.embed_dim),
dtype=self.point_embeddings[0].weight.dtype,
device=self.point_embeddings[0].weight.device,
)
if points is not None:
coords, labels = points
point_embeddings = self._embed_points(coords, labels, pad=(boxes is None))
sparse_embeddings = torch.cat([sparse_embeddings, point_embeddings], dim=1)
if boxes is not None:
box_embeddings = self._embed_boxes(boxes)
sparse_embeddings = torch.cat([sparse_embeddings, box_embeddings], dim=1)
if masks is not None:
dense_embeddings = self._embed_masks(masks)
else:
dense_embeddings = self.no_mask_embed.weight.reshape(1, -1, 1, 1).expand(
bs, -1, self.image_embedding_size[0], self.image_embedding_size[1]
)
return sparse_embeddings, dense_embeddings
class MemoryEncoder(nn.Module):
"""Encode pixel features and masks into a memory representation for efficient image segmentation.
This class processes pixel-level features and masks, fusing them to generate encoded memory representations suitable
for downstream tasks in image segmentation models like SAM (Segment Anything Model).
Attributes:
mask_downsampler (MaskDownSampler): Module for downsampling input masks.
pix_feat_proj (nn.Conv2d): Convolutional layer for projecting pixel features.
fuser (Fuser): Module for fusing pixel features and masks.
position_encoding (PositionEmbeddingSine): Module for adding positional encoding to features.
out_proj (nn.Module): Output projection layer, either nn.Identity or nn.Conv2d.
Methods:
forward: Process input pixel features and masks to generate encoded memory representations.
Examples:
>>> import torch
>>> encoder = MemoryEncoder(out_dim=256, in_dim=256)
>>> pix_feat = torch.randn(1, 256, 64, 64)
>>> masks = torch.randn(1, 1, 64, 64)
>>> encoded_feat, pos = encoder(pix_feat, masks)
>>> print(encoded_feat.shape, pos.shape)
torch.Size([1, 256, 64, 64]) torch.Size([1, 128, 64, 64])
"""
def __init__(
self,
out_dim,
in_dim=256, # in_dim of pix_feats
interpol_size: tuple[int, int] | None = None,
):
"""Initialize the MemoryEncoder for encoding pixel features and masks into memory representations.
This encoder processes pixel-level features and masks, fusing them to generate encoded memory representations
suitable for downstream tasks in image segmentation models like SAM (Segment Anything Model).
Args:
out_dim (int): Output dimension of the encoded features.
in_dim (int): Input dimension of the pixel features.
interpol_size (tuple[int, int] | None): Size to interpolate masks to. If None, uses the size of pixel
features.
"""
super().__init__()
self.mask_downsampler = MaskDownSampler(kernel_size=3, stride=2, padding=1, interpol_size=interpol_size)
self.pix_feat_proj = nn.Conv2d(in_dim, in_dim, kernel_size=1)
self.fuser = Fuser(CXBlock(dim=256), num_layers=2)
self.position_encoding = PositionEmbeddingSine(num_pos_feats=64)
self.out_proj = nn.Identity()
if out_dim != in_dim:
self.out_proj = nn.Conv2d(in_dim, out_dim, kernel_size=1)
def forward(
self,
pix_feat: torch.Tensor,
masks: torch.Tensor,
skip_mask_sigmoid: bool = False,
) -> dict:
"""Process pixel features and masks to generate encoded memory representations for segmentation."""
if not skip_mask_sigmoid:
masks = F.sigmoid(masks)
masks = self.mask_downsampler(masks)
# Fuse pix_feats and downsampled masks, in case the visual features are on CPU, cast them to CUDA
pix_feat = pix_feat.to(masks.device)
x = self.pix_feat_proj(pix_feat)
x = x + masks
x = self.fuser(x)
x = self.out_proj(x)
pos = self.position_encoding(x).to(x.dtype)
return {"vision_features": x, "vision_pos_enc": [pos]}
class ImageEncoder(nn.Module):
"""Encode images using a trunk-neck architecture, producing multiscale features and positional encodings.
This class combines a trunk network for feature extraction with a neck network for feature refinement and positional
encoding generation. It can optionally discard the lowest resolution features.
Attributes:
trunk (nn.Module): The trunk network for initial feature extraction.
neck (nn.Module): The neck network for feature refinement and positional encoding generation.
scalp (int): Number of lowest resolution feature levels to discard.
Methods:
forward: Process the input image through the trunk and neck networks.
Examples:
>>> trunk = SomeTrunkNetwork()
>>> neck = SomeNeckNetwork()
>>> encoder = ImageEncoder(trunk, neck, scalp=1)
>>> image = torch.randn(1, 3, 224, 224)
>>> output = encoder(image)
>>> print(output.keys())
dict_keys(['vision_features', 'vision_pos_enc', 'backbone_fpn'])
"""
def __init__(
self,
trunk: nn.Module,
neck: nn.Module,
scalp: int = 0,
):
"""Initialize the ImageEncoder with trunk and neck networks for feature extraction and refinement.
This encoder combines a trunk network for feature extraction with a neck network for feature refinement and
positional encoding generation. It can optionally discard the lowest resolution features.
Args:
trunk (nn.Module): The trunk network for initial feature extraction.
neck (nn.Module): The neck network for feature refinement and positional encoding generation.
scalp (int): Number of lowest resolution feature levels to discard.
"""
super().__init__()
self.trunk = trunk
self.neck = neck
self.scalp = scalp
assert self.trunk.channel_list == self.neck.backbone_channel_list, (
f"Channel dims of trunk {self.trunk.channel_list} and neck {self.neck.backbone_channel_list} do not match."
)
def forward(self, sample: torch.Tensor):
"""Encode input through trunk and neck networks, returning multiscale features and positional encodings."""
features, pos = self.neck(self.trunk(sample))
if self.scalp > 0:
# Discard the lowest resolution features
features, pos = features[: -self.scalp], pos[: -self.scalp]
src = features[-1]
return {
"vision_features": src,
"vision_pos_enc": pos,
"backbone_fpn": features,
}
class FpnNeck(nn.Module):
"""A Feature Pyramid Network (FPN) neck variant for multiscale feature fusion in object detection models.
This FPN variant removes the output convolution and uses bicubic interpolation for feature resizing, similar to ViT
positional embedding interpolation.
Attributes:
position_encoding (PositionEmbeddingSine): Sinusoidal positional encoding module.
convs (nn.ModuleList): List of convolutional layers for each backbone level.
backbone_channel_list (list[int]): List of channel dimensions from the backbone.
fpn_interp_model (str): Interpolation mode for FPN feature resizing.
fuse_type (str): Type of feature fusion, either 'sum' or 'avg'.
fpn_top_down_levels (list[int]): Levels to have top-down features in outputs.
Methods:
forward: Perform forward pass through the FPN neck.
Examples:
>>> backbone_channels = [64, 128, 256, 512]
>>> fpn_neck = FpnNeck(256, backbone_channels)
>>> inputs = [torch.rand(1, c, 32, 32) for c in backbone_channels]
>>> outputs, positions = fpn_neck(inputs)
>>> print(len(outputs), len(positions))
4 4
"""
def __init__(
self,
d_model: int,
backbone_channel_list: list[int],
kernel_size: int = 1,
stride: int = 1,
padding: int = 0,
fpn_interp_model: str = "bilinear",
fuse_type: str = "sum",
fpn_top_down_levels: list[int] | None = None,
):
"""Initialize a modified Feature Pyramid Network (FPN) neck.
This FPN variant removes the output convolution and uses bicubic interpolation for feature resizing, similar to
ViT positional embedding interpolation.
Args:
d_model (int): Dimension of the model.
backbone_channel_list (list[int]): List of channel dimensions from the backbone.
kernel_size (int): Kernel size for the convolutional layers.
stride (int): Stride for the convolutional layers.
padding (int): Padding for the convolutional layers.
fpn_interp_model (str): Interpolation mode for FPN feature resizing.
fuse_type (str): Type of feature fusion, either 'sum' or 'avg'.
fpn_top_down_levels (Optional[list[int]]): Levels to have top-down features in outputs.
"""
super().__init__()
self.position_encoding = PositionEmbeddingSine(num_pos_feats=256)
self.convs = nn.ModuleList()
self.backbone_channel_list = backbone_channel_list
for dim in backbone_channel_list:
current = nn.Sequential()
current.add_module(
"conv",
nn.Conv2d(
in_channels=dim,
out_channels=d_model,
kernel_size=kernel_size,
stride=stride,
padding=padding,
),
)
self.convs.append(current)
self.fpn_interp_model = fpn_interp_model
assert fuse_type in {"sum", "avg"}
self.fuse_type = fuse_type
# Levels to have top-down features in its outputs
# e.g. if fpn_top_down_levels is [2, 3], then only outputs of level 2 and 3
# have top-down propagation, while outputs of level 0 and level 1 have only
# lateral features from the same backbone level
if fpn_top_down_levels is None:
# Default is to have top-down features on all levels
fpn_top_down_levels = range(len(self.convs))
self.fpn_top_down_levels = list(fpn_top_down_levels)
def forward(self, xs: list[torch.Tensor]):
"""Perform forward pass through the Feature Pyramid Network (FPN) neck.
This method processes a list of input tensors from the backbone through the FPN, applying lateral connections
and top-down feature fusion. It generates output feature maps and corresponding positional encodings.
Args:
xs (list[torch.Tensor]): List of input tensors from the backbone, each with shape (B, C, H, W).
Returns:
out (list[torch.Tensor]): List of output feature maps after FPN processing, each with shape (B, d_model, H,
W).
pos (list[torch.Tensor]): List of positional encodings corresponding to each output feature map.
Examples:
>>> fpn_neck = FpnNeck(d_model=256, backbone_channel_list=[64, 128, 256, 512])
>>> inputs = [torch.rand(1, c, 32, 32) for c in [64, 128, 256, 512]]
>>> outputs, positions = fpn_neck(inputs)
>>> print(len(outputs), len(positions))
4 4
"""
out = [None] * len(self.convs)
pos = [None] * len(self.convs)
assert len(xs) == len(self.convs)
# FPN forward pass
# see https://github.com/facebookresearch/detectron2/blob/main/detectron2/modeling/backbone/fpn.py
prev_features = None
# Forward in top-down order (from low to high resolution)
n = len(self.convs) - 1
for i in range(n, -1, -1):
x = xs[i]
lateral_features = self.convs[n - i](x)
if i in self.fpn_top_down_levels and prev_features is not None:
top_down_features = F.interpolate(
prev_features.to(dtype=x.dtype),
scale_factor=2.0,
mode=self.fpn_interp_model,
align_corners=(None if self.fpn_interp_model == "nearest" else False),
antialias=False,
)
prev_features = lateral_features + top_down_features
if self.fuse_type == "avg":
prev_features /= 2
else:
prev_features = lateral_features
x_out = prev_features
out[i] = x_out
pos[i] = self.position_encoding(x_out).to(x_out.dtype)
return out, pos
class Hiera(nn.Module):
"""Hierarchical vision transformer for efficient multiscale feature extraction in image processing tasks.
This class implements a Hiera model, which is a hierarchical vision transformer architecture designed for efficient
multiscale feature extraction. It uses a series of transformer blocks organized into stages, with optional pooling
and global attention mechanisms.
Attributes:
window_spec (tuple[int, ...]): Window sizes for each stage.
q_stride (tuple[int, int]): Downsampling stride between stages.
stage_ends (list[int]): Indices of the last block in each stage.
q_pool_blocks (list[int]): Indices of blocks where pooling is applied.
return_interm_layers (bool): Whether to return intermediate layer outputs.
patch_embed (PatchEmbed): Module for patch embedding.
global_att_blocks (tuple[int, ...]): Indices of blocks with global attention.
window_pos_embed_bkg_spatial_size (tuple[int, int]): Spatial size for window positional embedding background.
pos_embed (nn.Parameter): Positional embedding for the background.
pos_embed_window (nn.Parameter): Positional embedding for the window.
blocks (nn.ModuleList): List of MultiScaleBlock modules.
channel_list (list[int]): List of output channel dimensions for each stage.
Methods:
_get_pos_embed: Generate positional embeddings by interpolating and combining window and background embeddings.
forward: Perform the forward pass through the Hiera model.
Examples:
>>> model = Hiera(embed_dim=96, num_heads=1, stages=(2, 3, 16, 3))
>>> input_tensor = torch.randn(1, 3, 224, 224)
>>> output_features = model(input_tensor)
>>> for feat in output_features:
... print(feat.shape)
"""
def __init__(
self,
embed_dim: int = 96, # initial embed dim
num_heads: int = 1, # initial number of heads
drop_path_rate: float = 0.0, # stochastic depth
q_pool: int = 3, # number of q_pool stages
q_stride: tuple[int, int] = (2, 2), # downsample stride bet. stages
stages: tuple[int, ...] = (2, 3, 16, 3), # blocks per stage
dim_mul: float = 2.0, # dim_mul factor at stage shift
head_mul: float = 2.0, # head_mul factor at stage shift
window_pos_embed_bkg_spatial_size: tuple[int, int] = (14, 14),
# window size per stage, when not using global att.
window_spec: tuple[int, ...] = (
8,
4,
14,
7,
),
# global attn in these blocks
global_att_blocks: tuple[int, ...] = (
12,
16,
20,
),
return_interm_layers=True, # return feats from every stage
):
"""Initialize a Hiera model, a hierarchical vision transformer for efficient multiscale feature extraction.
Hiera is a hierarchical vision transformer architecture designed for efficient multiscale feature extraction in
image processing tasks. It uses a series of transformer blocks organized into stages, with optional pooling and
global attention mechanisms.
Args:
embed_dim (int): Initial embedding dimension for the model.
num_heads (int): Initial number of attention heads.
drop_path_rate (float): Stochastic depth rate.
q_pool (int): Number of query pooling stages.
q_stride (tuple[int, int]): Downsampling stride between stages.
stages (tuple[int, ...]): Number of blocks per stage.
dim_mul (float): Dimension multiplier factor at stage transitions.
head_mul (float): Head multiplier factor at stage transitions.
window_pos_embed_bkg_spatial_size (tuple[int, int]): Spatial size for window positional embedding
background.
window_spec (tuple[int, ...]): Window sizes for each stage when not using global attention.
global_att_blocks (tuple[int, ...]): Indices of blocks that use global attention.
return_interm_layers (bool): Whether to return intermediate layer outputs.
"""
super().__init__()
assert len(stages) == len(window_spec)
self.window_spec = window_spec
depth = sum(stages)
self.q_stride = q_stride
self.stage_ends = [sum(stages[:i]) - 1 for i in range(1, len(stages) + 1)]
assert 0 <= q_pool <= len(self.stage_ends[:-1])
self.q_pool_blocks = [x + 1 for x in self.stage_ends[:-1]][:q_pool]
self.return_interm_layers = return_interm_layers
self.patch_embed = PatchEmbed(
embed_dim=embed_dim,
kernel_size=(7, 7),
stride=(4, 4),
padding=(3, 3),
)
# Which blocks have global attention?
self.global_att_blocks = global_att_blocks
# Windowed positional embedding (https://arxiv.org/abs/2311.05613)
self.window_pos_embed_bkg_spatial_size = window_pos_embed_bkg_spatial_size
self.pos_embed = nn.Parameter(torch.zeros(1, embed_dim, *self.window_pos_embed_bkg_spatial_size))
self.pos_embed_window = nn.Parameter(torch.zeros(1, embed_dim, self.window_spec[0], self.window_spec[0]))
dpr = [x.item() for x in torch.linspace(0, drop_path_rate, depth)] # stochastic depth decay rule
cur_stage = 1
self.blocks = nn.ModuleList()
for i in range(depth):
dim_out = embed_dim
# Lags by a block, so first block of next stage uses an initial window size
# of previous stage and final window size of current stage
window_size = self.window_spec[cur_stage - 1]
if self.global_att_blocks is not None:
window_size = 0 if i in self.global_att_blocks else window_size
if i - 1 in self.stage_ends:
dim_out = int(embed_dim * dim_mul)
num_heads = int(num_heads * head_mul)
cur_stage += 1
block = MultiScaleBlock(
dim=embed_dim,
dim_out=dim_out,
num_heads=num_heads,
drop_path=dpr[i],
q_stride=self.q_stride if i in self.q_pool_blocks else None,
window_size=window_size,
)
embed_dim = dim_out
self.blocks.append(block)
self.channel_list = (
[self.blocks[i].dim_out for i in self.stage_ends[::-1]]
if return_interm_layers
else [self.blocks[-1].dim_out]
)
def _get_pos_embed(self, hw: tuple[int, int]) -> torch.Tensor:
"""Generate positional embeddings by interpolating and combining window and background embeddings."""
h, w = hw
window_embed = self.pos_embed_window
pos_embed = F.interpolate(self.pos_embed, size=(h, w), mode="bicubic")
pos_embed = pos_embed + window_embed.tile([x // y for x, y in zip(pos_embed.shape, window_embed.shape)])
pos_embed = pos_embed.permute(0, 2, 3, 1)
return pos_embed
def forward(self, x: torch.Tensor) -> list[torch.Tensor]:
"""Perform forward pass through Hiera model, extracting multiscale features from input images.
Args:
x (torch.Tensor): Input tensor with shape (B, C, H, W) representing a batch of images.
Returns:
(list[torch.Tensor]): List of feature maps at different scales, each with shape (B, C_i, H_i, W_i), where
C_i is the channel dimension and H_i, W_i are the spatial dimensions at scale i. The list is ordered
from highest resolution (fine features) to lowest resolution (coarse features) if return_interm_layers
is True, otherwise contains only the final output.
Examples:
>>> model = Hiera(embed_dim=96, num_heads=1, stages=(2, 3, 16, 3))
>>> input_tensor = torch.randn(1, 3, 224, 224)
>>> output_features = model(input_tensor)
>>> for feat in output_features:
... print(feat.shape)
"""
x = self.patch_embed(x)
# x: (B, H, W, C)
# Add positional embedding
x = x + self._get_pos_embed(x.shape[1:3])
outputs = []
for i, blk in enumerate(self.blocks):
x = blk(x)
if (i == self.stage_ends[-1]) or (i in self.stage_ends and self.return_interm_layers):
feats = x.permute(0, 3, 1, 2)
outputs.append(feats)
return outputs

View File

@@ -0,0 +1,298 @@
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
from __future__ import annotations
import copy
import torch
from torch import nn
from .blocks import RoPEAttention
class MemoryAttentionLayer(nn.Module):
"""Implements a memory attention layer with self-attention and cross-attention mechanisms for neural networks.
This class combines self-attention, cross-attention, and feedforward components to process input tensors and
generate memory-based attention outputs.
Attributes:
d_model (int): Dimensionality of the model.
dim_feedforward (int): Dimensionality of the feedforward network.
dropout_value (float): Dropout rate for regularization.
self_attn (RoPEAttention): Self-attention mechanism using RoPE (Rotary Position Embedding).
cross_attn_image (RoPEAttention): Cross-attention mechanism for image processing.
linear1 (nn.Linear): First linear layer of the feedforward network.
linear2 (nn.Linear): Second linear layer of the feedforward network.
norm1 (nn.LayerNorm): Layer normalization for self-attention output.
norm2 (nn.LayerNorm): Layer normalization for cross-attention output.
norm3 (nn.LayerNorm): Layer normalization for feedforward network output.
dropout1 (nn.Dropout): Dropout layer after self-attention.
dropout2 (nn.Dropout): Dropout layer after cross-attention.
dropout3 (nn.Dropout): Dropout layer after feedforward network.
activation (nn.ReLU): Activation function for the feedforward network.
pos_enc_at_attn (bool): Flag to add positional encoding at attention.
pos_enc_at_cross_attn_queries (bool): Flag to add positional encoding to cross-attention queries.
pos_enc_at_cross_attn_keys (bool): Flag to add positional encoding to cross-attention keys.
Methods:
forward: Performs the full memory attention operation on input tensors.
_forward_sa: Performs self-attention on input tensor.
_forward_ca: Performs cross-attention between target and memory tensors.
Examples:
>>> layer = MemoryAttentionLayer(d_model=256, dim_feedforward=2048, dropout=0.1)
>>> tgt = torch.randn(1, 100, 256)
>>> memory = torch.randn(1, 100, 64)
>>> pos = torch.randn(1, 100, 256)
>>> query_pos = torch.randn(1, 100, 256)
>>> output = layer(tgt, memory, pos, query_pos)
>>> print(output.shape)
torch.Size([1, 100, 256])
"""
def __init__(
self,
d_model: int = 256,
dim_feedforward: int = 2048,
dropout: float = 0.1,
pos_enc_at_attn: bool = False,
pos_enc_at_cross_attn_keys: bool = True,
pos_enc_at_cross_attn_queries: bool = False,
self_attn: nn.Module | None = None,
cross_attn: nn.Module | None = None,
):
"""Initialize a memory attention layer with self-attention, cross-attention, and feedforward components.
Args:
d_model (int): Dimensionality of the model.
dim_feedforward (int): Dimensionality of the feedforward network.
dropout (float): Dropout rate for regularization.
pos_enc_at_attn (bool): Whether to add positional encoding at attention.
pos_enc_at_cross_attn_keys (bool): Whether to add positional encoding to cross-attention keys.
pos_enc_at_cross_attn_queries (bool): Whether to add positional encoding to cross-attention queries.
self_attn (nn.Module | None): Custom self-attention module. If None, a default RoPEAttention is used.
cross_attn (nn.Module | None): Custom cross-attention module. If None, a default RoPEAttention is used.
"""
super().__init__()
self.d_model = d_model
self.dim_feedforward = dim_feedforward
self.dropout_value = dropout
self.self_attn = self_attn or RoPEAttention(embedding_dim=256, num_heads=1, downsample_rate=1)
self.cross_attn_image = cross_attn or RoPEAttention(
rope_k_repeat=True,
embedding_dim=256,
num_heads=1,
downsample_rate=1,
kv_in_dim=64,
)
# Implementation of Feedforward model
self.linear1 = nn.Linear(d_model, dim_feedforward)
self.dropout = nn.Dropout(dropout)
self.linear2 = nn.Linear(dim_feedforward, d_model)
self.norm1 = nn.LayerNorm(d_model)
self.norm2 = nn.LayerNorm(d_model)
self.norm3 = nn.LayerNorm(d_model)
self.dropout1 = nn.Dropout(dropout)
self.dropout2 = nn.Dropout(dropout)
self.dropout3 = nn.Dropout(dropout)
self.activation = nn.ReLU()
# Where to add pos enc
self.pos_enc_at_attn = pos_enc_at_attn
self.pos_enc_at_cross_attn_queries = pos_enc_at_cross_attn_queries
self.pos_enc_at_cross_attn_keys = pos_enc_at_cross_attn_keys
def _forward_sa(self, tgt: torch.Tensor, query_pos: torch.Tensor | None) -> torch.Tensor:
"""Perform self-attention on input tensor using positional encoding and RoPE attention mechanism."""
tgt2 = self.norm1(tgt)
q = k = tgt2 + query_pos if self.pos_enc_at_attn else tgt2
tgt2 = self.self_attn(q, k, v=tgt2)
tgt = tgt + self.dropout1(tgt2)
return tgt
def _forward_ca(
self,
tgt: torch.Tensor,
memory: torch.Tensor,
query_pos: torch.Tensor | None,
pos: torch.Tensor | None,
num_k_exclude_rope: int = 0,
) -> torch.Tensor:
"""Perform cross-attention between target and memory tensors using RoPEAttention mechanism."""
kwds = {}
if num_k_exclude_rope > 0:
assert isinstance(self.cross_attn_image, RoPEAttention)
kwds = {"num_k_exclude_rope": num_k_exclude_rope}
# Cross-Attention
tgt2 = self.norm2(tgt)
tgt2 = self.cross_attn_image(
q=tgt2 + query_pos if self.pos_enc_at_cross_attn_queries else tgt2,
k=memory + pos if self.pos_enc_at_cross_attn_keys else memory,
v=memory,
**kwds,
)
tgt = tgt + self.dropout2(tgt2)
return tgt
def forward(
self,
tgt: torch.Tensor,
memory: torch.Tensor,
pos: torch.Tensor | None = None,
query_pos: torch.Tensor | None = None,
num_k_exclude_rope: int = 0,
) -> torch.Tensor:
"""Process input tensors through self-attention, cross-attention, and feedforward network layers.
Args:
tgt (torch.Tensor): Target tensor for self-attention with shape (N, L, D).
memory (torch.Tensor): Memory tensor for cross-attention with shape (N, S, D).
pos (Optional[torch.Tensor]): Positional encoding for memory tensor.
query_pos (Optional[torch.Tensor]): Positional encoding for target tensor.
num_k_exclude_rope (int): Number of keys to exclude from rotary position embedding.
Returns:
(torch.Tensor): Processed tensor after attention and feedforward layers with shape (N, L, D).
"""
tgt = self._forward_sa(tgt, query_pos)
tgt = self._forward_ca(tgt, memory, query_pos, pos, num_k_exclude_rope)
# MLP
tgt2 = self.norm3(tgt)
tgt2 = self.linear2(self.dropout(self.activation(self.linear1(tgt2))))
tgt = tgt + self.dropout3(tgt2)
return tgt
class MemoryAttention(nn.Module):
"""Memory attention module for processing sequential data with self and cross-attention mechanisms.
This class implements a multi-layer attention mechanism that combines self-attention and cross-attention for
processing sequential data, particularly useful in transformer-like architectures.
Attributes:
d_model (int): The dimension of the model's hidden state.
layers (nn.ModuleList): A list of MemoryAttentionLayer modules.
num_layers (int): The number of attention layers.
norm (nn.LayerNorm): Layer normalization applied to the output.
pos_enc_at_input (bool): Whether to apply positional encoding at the input.
batch_first (bool): Whether the input tensors are in batch-first format.
Methods:
forward: Processes input tensors through the attention layers.
Examples:
>>> d_model = 256
>>> layer = MemoryAttentionLayer(d_model)
>>> attention = MemoryAttention(d_model, pos_enc_at_input=True, layer=layer, num_layers=3)
>>> curr = torch.randn(10, 32, d_model) # (seq_len, batch_size, d_model)
>>> memory = torch.randn(20, 32, d_model) # (mem_len, batch_size, d_model)
>>> curr_pos = torch.randn(10, 32, d_model)
>>> memory_pos = torch.randn(20, 32, d_model)
>>> output = attention(curr, memory, curr_pos, memory_pos)
>>> print(output.shape)
torch.Size([10, 32, 256])
"""
def __init__(
self,
d_model: int,
pos_enc_at_input: bool,
layer: nn.Module,
num_layers: int,
batch_first: bool = True, # Do layers expect batch first input?
):
"""Initialize MemoryAttention with specified layers and normalization for sequential data processing.
This class implements a multi-layer attention mechanism that combines self-attention and cross-attention for
processing sequential data, particularly useful in transformer-like architectures.
Args:
d_model (int): The dimension of the model's hidden state.
pos_enc_at_input (bool): Whether to apply positional encoding at the input.
layer (nn.Module): The attention layer to be used in the module.
num_layers (int): The number of attention layers.
batch_first (bool): Whether the input tensors are in batch-first format.
"""
super().__init__()
self.d_model = d_model
self.layers = nn.ModuleList([copy.deepcopy(layer) for _ in range(num_layers)])
self.num_layers = num_layers
self.norm = nn.LayerNorm(d_model)
self.pos_enc_at_input = pos_enc_at_input
self.batch_first = batch_first
def forward(
self,
curr: torch.Tensor, # self-attention inputs
memory: torch.Tensor, # cross-attention inputs
curr_pos: torch.Tensor | None = None, # pos_enc for self-attention inputs
memory_pos: torch.Tensor | None = None, # pos_enc for cross-attention inputs
num_obj_ptr_tokens: int = 0, # number of object pointer *tokens*
) -> torch.Tensor:
"""Process inputs through attention layers, applying self and cross-attention with positional encoding.
Args:
curr (torch.Tensor): Self-attention input tensor, representing the current state.
memory (torch.Tensor): Cross-attention input tensor, representing memory information.
curr_pos (Optional[torch.Tensor]): Positional encoding for self-attention inputs.
memory_pos (Optional[torch.Tensor]): Positional encoding for cross-attention inputs.
num_obj_ptr_tokens (int): Number of object pointer tokens to exclude from rotary position embedding.
Returns:
(torch.Tensor): Processed output tensor after applying attention layers and normalization.
Examples:
>>> d_model = 256
>>> layer = MemoryAttentionLayer(d_model)
>>> attention = MemoryAttention(d_model, pos_enc_at_input=True, layer=layer, num_layers=3)
>>> curr = torch.randn(10, 32, d_model) # (seq_len, batch_size, d_model)
>>> memory = torch.randn(20, 32, d_model) # (mem_len, batch_size, d_model)
>>> curr_pos = torch.randn(10, 32, d_model)
>>> memory_pos = torch.randn(20, 32, d_model)
>>> output = attention(curr, memory, curr_pos, memory_pos)
>>> print(output.shape)
torch.Size([10, 32, 256])
"""
if isinstance(curr, list):
assert isinstance(curr_pos, list)
assert len(curr) == len(curr_pos) == 1
curr, curr_pos = curr[0], curr_pos[0]
assert curr.shape[1] == memory.shape[1], "Batch size must be the same for curr and memory"
output = curr
if self.pos_enc_at_input and curr_pos is not None:
output = output + 0.1 * curr_pos
if self.batch_first:
# Convert to batch first
output = output.transpose(0, 1)
curr_pos = curr_pos.transpose(0, 1)
memory = memory.transpose(0, 1)
memory_pos = memory_pos.transpose(0, 1)
for layer in self.layers:
kwds = {}
if isinstance(layer.cross_attn_image, RoPEAttention):
kwds = {"num_k_exclude_rope": num_obj_ptr_tokens}
output = layer(
tgt=output,
memory=memory,
pos=memory_pos,
query_pos=curr_pos,
**kwds,
)
normed_output = self.norm(output)
if self.batch_first:
# Convert back to seq first
normed_output = normed_output.transpose(0, 1)
curr_pos = curr_pos.transpose(0, 1)
return normed_output

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,979 @@
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
# --------------------------------------------------------
# TinyViT Model Architecture
# Copyright (c) 2022 Microsoft
# Adapted from LeViT and Swin Transformer
# LeViT: (https://github.com/facebookresearch/levit)
# Swin: (https://github.com/microsoft/swin-transformer)
# Build the TinyViT Model
# --------------------------------------------------------
from __future__ import annotations
import itertools
import torch
import torch.nn as nn
import torch.nn.functional as F
from ultralytics.nn.modules import LayerNorm2d
from ultralytics.utils.instance import to_2tuple
class Conv2d_BN(torch.nn.Sequential):
"""A sequential container that performs 2D convolution followed by batch normalization.
This module combines a 2D convolution layer with batch normalization, providing a common building block for
convolutional neural networks. The batch normalization weights and biases are initialized to specific values for
optimal training performance.
Attributes:
c (torch.nn.Conv2d): 2D convolution layer.
bn (torch.nn.BatchNorm2d): Batch normalization layer.
Examples:
>>> conv_bn = Conv2d_BN(3, 64, ks=3, stride=1, pad=1)
>>> input_tensor = torch.randn(1, 3, 224, 224)
>>> output = conv_bn(input_tensor)
>>> print(output.shape)
torch.Size([1, 64, 224, 224])
"""
def __init__(
self,
a: int,
b: int,
ks: int = 1,
stride: int = 1,
pad: int = 0,
dilation: int = 1,
groups: int = 1,
bn_weight_init: float = 1,
):
"""Initialize a sequential container with 2D convolution followed by batch normalization.
Args:
a (int): Number of input channels.
b (int): Number of output channels.
ks (int, optional): Kernel size for the convolution.
stride (int, optional): Stride for the convolution.
pad (int, optional): Padding for the convolution.
dilation (int, optional): Dilation factor for the convolution.
groups (int, optional): Number of groups for the convolution.
bn_weight_init (float, optional): Initial value for batch normalization weight.
"""
super().__init__()
self.add_module("c", torch.nn.Conv2d(a, b, ks, stride, pad, dilation, groups, bias=False))
bn = torch.nn.BatchNorm2d(b)
torch.nn.init.constant_(bn.weight, bn_weight_init)
torch.nn.init.constant_(bn.bias, 0)
self.add_module("bn", bn)
class PatchEmbed(nn.Module):
"""Embed images into patches and project them into a specified embedding dimension.
This module converts input images into patch embeddings using a sequence of convolutional layers, effectively
downsampling the spatial dimensions while increasing the channel dimension.
Attributes:
patches_resolution (tuple[int, int]): Resolution of the patches after embedding.
num_patches (int): Total number of patches.
in_chans (int): Number of input channels.
embed_dim (int): Dimension of the embedding.
seq (nn.Sequential): Sequence of convolutional and activation layers for patch embedding.
Examples:
>>> import torch
>>> patch_embed = PatchEmbed(in_chans=3, embed_dim=96, resolution=224, activation=nn.GELU)
>>> x = torch.randn(1, 3, 224, 224)
>>> output = patch_embed(x)
>>> print(output.shape)
torch.Size([1, 96, 56, 56])
"""
def __init__(self, in_chans: int, embed_dim: int, resolution: int, activation):
"""Initialize patch embedding with convolutional layers for image-to-patch conversion and projection.
Args:
in_chans (int): Number of input channels.
embed_dim (int): Dimension of the embedding.
resolution (int): Input image resolution.
activation (nn.Module): Activation function to use between convolutions.
"""
super().__init__()
img_size: tuple[int, int] = to_2tuple(resolution)
self.patches_resolution = (img_size[0] // 4, img_size[1] // 4)
self.num_patches = self.patches_resolution[0] * self.patches_resolution[1]
self.in_chans = in_chans
self.embed_dim = embed_dim
n = embed_dim
self.seq = nn.Sequential(
Conv2d_BN(in_chans, n // 2, 3, 2, 1),
activation(),
Conv2d_BN(n // 2, n, 3, 2, 1),
)
def forward(self, x: torch.Tensor) -> torch.Tensor:
"""Process input tensor through patch embedding sequence, converting images to patch embeddings."""
return self.seq(x)
class MBConv(nn.Module):
"""Mobile Inverted Bottleneck Conv (MBConv) layer, part of the EfficientNet architecture.
This module implements the mobile inverted bottleneck convolution with expansion, depthwise convolution, and
projection phases, along with residual connections for improved gradient flow.
Attributes:
in_chans (int): Number of input channels.
hidden_chans (int): Number of hidden channels after expansion.
out_chans (int): Number of output channels.
conv1 (Conv2d_BN): First convolutional layer for channel expansion.
act1 (nn.Module): First activation function.
conv2 (Conv2d_BN): Depthwise convolutional layer.
act2 (nn.Module): Second activation function.
conv3 (Conv2d_BN): Final convolutional layer for projection.
act3 (nn.Module): Third activation function.
drop_path (nn.Module): Drop path layer (Identity for inference).
Examples:
>>> in_chans, out_chans = 32, 64
>>> mbconv = MBConv(in_chans, out_chans, expand_ratio=4, activation=nn.ReLU, drop_path=0.1)
>>> x = torch.randn(1, in_chans, 56, 56)
>>> output = mbconv(x)
>>> print(output.shape)
torch.Size([1, 64, 56, 56])
"""
def __init__(self, in_chans: int, out_chans: int, expand_ratio: float, activation, drop_path: float):
"""Initialize the MBConv layer with specified input/output channels, expansion ratio, and activation.
Args:
in_chans (int): Number of input channels.
out_chans (int): Number of output channels.
expand_ratio (float): Channel expansion ratio for the hidden layer.
activation (nn.Module): Activation function to use.
drop_path (float): Drop path rate for stochastic depth.
"""
super().__init__()
self.in_chans = in_chans
self.hidden_chans = int(in_chans * expand_ratio)
self.out_chans = out_chans
self.conv1 = Conv2d_BN(in_chans, self.hidden_chans, ks=1)
self.act1 = activation()
self.conv2 = Conv2d_BN(self.hidden_chans, self.hidden_chans, ks=3, stride=1, pad=1, groups=self.hidden_chans)
self.act2 = activation()
self.conv3 = Conv2d_BN(self.hidden_chans, out_chans, ks=1, bn_weight_init=0.0)
self.act3 = activation()
# NOTE: `DropPath` is needed only for training.
# self.drop_path = DropPath(drop_path) if drop_path > 0. else nn.Identity()
self.drop_path = nn.Identity()
def forward(self, x: torch.Tensor) -> torch.Tensor:
"""Implement the forward pass of MBConv, applying convolutions and skip connection."""
shortcut = x
x = self.conv1(x)
x = self.act1(x)
x = self.conv2(x)
x = self.act2(x)
x = self.conv3(x)
x = self.drop_path(x)
x += shortcut
return self.act3(x)
class PatchMerging(nn.Module):
"""Merge neighboring patches in the feature map and project to a new dimension.
This class implements a patch merging operation that combines spatial information and adjusts the feature dimension
using a series of convolutional layers with batch normalization. It effectively reduces spatial resolution while
potentially increasing channel dimensions.
Attributes:
input_resolution (tuple[int, int]): The input resolution (height, width) of the feature map.
dim (int): The input dimension of the feature map.
out_dim (int): The output dimension after merging and projection.
act (nn.Module): The activation function used between convolutions.
conv1 (Conv2d_BN): The first convolutional layer for dimension projection.
conv2 (Conv2d_BN): The second convolutional layer for spatial merging.
conv3 (Conv2d_BN): The third convolutional layer for final projection.
Examples:
>>> input_resolution = (56, 56)
>>> patch_merging = PatchMerging(input_resolution, dim=64, out_dim=128, activation=nn.ReLU)
>>> x = torch.randn(4, 64, 56, 56)
>>> output = patch_merging(x)
>>> print(output.shape)
torch.Size([4, 3136, 128])
"""
def __init__(self, input_resolution: tuple[int, int], dim: int, out_dim: int, activation):
"""Initialize the PatchMerging module for merging and projecting neighboring patches in feature maps.
Args:
input_resolution (tuple[int, int]): The input resolution (height, width) of the feature map.
dim (int): The input dimension of the feature map.
out_dim (int): The output dimension after merging and projection.
activation (nn.Module): The activation function used between convolutions.
"""
super().__init__()
self.input_resolution = input_resolution
self.dim = dim
self.out_dim = out_dim
self.act = activation()
self.conv1 = Conv2d_BN(dim, out_dim, 1, 1, 0)
stride_c = 1 if out_dim in {320, 448, 576} else 2
self.conv2 = Conv2d_BN(out_dim, out_dim, 3, stride_c, 1, groups=out_dim)
self.conv3 = Conv2d_BN(out_dim, out_dim, 1, 1, 0)
def forward(self, x: torch.Tensor) -> torch.Tensor:
"""Apply patch merging and dimension projection to the input feature map."""
if x.ndim == 3:
H, W = self.input_resolution
B = len(x)
# (B, C, H, W)
x = x.view(B, H, W, -1).permute(0, 3, 1, 2)
x = self.conv1(x)
x = self.act(x)
x = self.conv2(x)
x = self.act(x)
x = self.conv3(x)
return x.flatten(2).transpose(1, 2)
class ConvLayer(nn.Module):
"""Convolutional Layer featuring multiple MobileNetV3-style inverted bottleneck convolutions (MBConv).
This layer optionally applies downsample operations to the output and supports gradient checkpointing for memory
efficiency during training.
Attributes:
dim (int): Dimensionality of the input and output.
input_resolution (tuple[int, int]): Resolution of the input image.
depth (int): Number of MBConv layers in the block.
use_checkpoint (bool): Whether to use gradient checkpointing to save memory.
blocks (nn.ModuleList): List of MBConv layers.
downsample (Optional[nn.Module]): Function for downsampling the output.
Examples:
>>> input_tensor = torch.randn(1, 64, 56, 56)
>>> conv_layer = ConvLayer(64, (56, 56), depth=3, activation=nn.ReLU)
>>> output = conv_layer(input_tensor)
>>> print(output.shape)
torch.Size([1, 3136, 128])
"""
def __init__(
self,
dim: int,
input_resolution: tuple[int, int],
depth: int,
activation,
drop_path: float | list[float] = 0.0,
downsample: nn.Module | None = None,
use_checkpoint: bool = False,
out_dim: int | None = None,
conv_expand_ratio: float = 4.0,
):
"""Initialize the ConvLayer with the given dimensions and settings.
This layer consists of multiple MobileNetV3-style inverted bottleneck convolutions (MBConv) and optionally
applies downsampling to the output.
Args:
dim (int): The dimensionality of the input and output.
input_resolution (tuple[int, int]): The resolution of the input image.
depth (int): The number of MBConv layers in the block.
activation (nn.Module): Activation function applied after each convolution.
drop_path (float | list[float], optional): Drop path rate. Single float or a list of floats for each MBConv.
downsample (Optional[nn.Module], optional): Function for downsampling the output. None to skip downsampling.
use_checkpoint (bool, optional): Whether to use gradient checkpointing to save memory.
out_dim (Optional[int], optional): Output dimensions. None means it will be the same as `dim`.
conv_expand_ratio (float, optional): Expansion ratio for the MBConv layers.
"""
super().__init__()
self.dim = dim
self.input_resolution = input_resolution
self.depth = depth
self.use_checkpoint = use_checkpoint
# Build blocks
self.blocks = nn.ModuleList(
[
MBConv(
dim,
dim,
conv_expand_ratio,
activation,
drop_path[i] if isinstance(drop_path, list) else drop_path,
)
for i in range(depth)
]
)
# Patch merging layer
self.downsample = (
None
if downsample is None
else downsample(input_resolution, dim=dim, out_dim=out_dim, activation=activation)
)
def forward(self, x: torch.Tensor) -> torch.Tensor:
"""Process input through convolutional layers, applying MBConv blocks and optional downsampling."""
for blk in self.blocks:
x = torch.utils.checkpoint(blk, x) if self.use_checkpoint else blk(x) # warn: checkpoint is slow import
return x if self.downsample is None else self.downsample(x)
class MLP(nn.Module):
"""Multi-layer Perceptron (MLP) module for transformer architectures.
This module applies layer normalization, two fully-connected layers with an activation function in between, and
dropout. It is commonly used in transformer-based architectures for processing token embeddings.
Attributes:
norm (nn.LayerNorm): Layer normalization applied to the input.
fc1 (nn.Linear): First fully-connected layer.
fc2 (nn.Linear): Second fully-connected layer.
act (nn.Module): Activation function applied after the first fully-connected layer.
drop (nn.Dropout): Dropout layer applied after the activation function.
Examples:
>>> import torch
>>> from torch import nn
>>> mlp = MLP(in_features=256, hidden_features=512, out_features=256, activation=nn.GELU, drop=0.1)
>>> x = torch.randn(32, 100, 256)
>>> output = mlp(x)
>>> print(output.shape)
torch.Size([32, 100, 256])
"""
def __init__(
self,
in_features: int,
hidden_features: int | None = None,
out_features: int | None = None,
activation=nn.GELU,
drop: float = 0.0,
):
"""Initialize a multi-layer perceptron with configurable input, hidden, and output dimensions.
Args:
in_features (int): Number of input features.
hidden_features (Optional[int], optional): Number of hidden features.
out_features (Optional[int], optional): Number of output features.
activation (nn.Module): Activation function applied after the first fully-connected layer.
drop (float, optional): Dropout probability.
"""
super().__init__()
out_features = out_features or in_features
hidden_features = hidden_features or in_features
self.norm = nn.LayerNorm(in_features)
self.fc1 = nn.Linear(in_features, hidden_features)
self.fc2 = nn.Linear(hidden_features, out_features)
self.act = activation()
self.drop = nn.Dropout(drop)
def forward(self, x: torch.Tensor) -> torch.Tensor:
"""Apply MLP operations: layer norm, FC layers, activation, and dropout to the input tensor."""
x = self.norm(x)
x = self.fc1(x)
x = self.act(x)
x = self.drop(x)
x = self.fc2(x)
return self.drop(x)
class Attention(torch.nn.Module):
"""Multi-head attention module with spatial awareness and trainable attention biases.
This module implements a multi-head attention mechanism with support for spatial awareness, applying attention
biases based on spatial resolution. It includes trainable attention biases for each unique offset between spatial
positions in the resolution grid.
Attributes:
num_heads (int): Number of attention heads.
scale (float): Scaling factor for attention scores.
key_dim (int): Dimensionality of the keys and queries.
nh_kd (int): Product of num_heads and key_dim.
d (int): Dimensionality of the value vectors.
dh (int): Product of d and num_heads.
attn_ratio (float): Attention ratio affecting the dimensions of the value vectors.
norm (nn.LayerNorm): Layer normalization applied to input.
qkv (nn.Linear): Linear layer for computing query, key, and value projections.
proj (nn.Linear): Linear layer for final projection.
attention_biases (nn.Parameter): Learnable attention biases.
attention_bias_idxs (torch.Tensor): Indices for attention biases.
ab (torch.Tensor): Cached attention biases for inference, deleted during training.
Examples:
>>> attn = Attention(dim=256, key_dim=64, num_heads=8, resolution=(14, 14))
>>> x = torch.randn(1, 196, 256)
>>> output = attn(x)
>>> print(output.shape)
torch.Size([1, 196, 256])
"""
def __init__(
self,
dim: int,
key_dim: int,
num_heads: int = 8,
attn_ratio: float = 4,
resolution: tuple[int, int] = (14, 14),
):
"""Initialize the Attention module for multi-head attention with spatial awareness.
This module implements a multi-head attention mechanism with support for spatial awareness, applying attention
biases based on spatial resolution. It includes trainable attention biases for each unique offset between
spatial positions in the resolution grid.
Args:
dim (int): The dimensionality of the input and output.
key_dim (int): The dimensionality of the keys and queries.
num_heads (int, optional): Number of attention heads.
attn_ratio (float, optional): Attention ratio, affecting the dimensions of the value vectors.
resolution (tuple[int, int], optional): Spatial resolution of the input feature map.
"""
super().__init__()
assert isinstance(resolution, tuple) and len(resolution) == 2, "'resolution' argument not tuple of length 2"
self.num_heads = num_heads
self.scale = key_dim**-0.5
self.key_dim = key_dim
self.nh_kd = nh_kd = key_dim * num_heads
self.d = int(attn_ratio * key_dim)
self.dh = int(attn_ratio * key_dim) * num_heads
self.attn_ratio = attn_ratio
h = self.dh + nh_kd * 2
self.norm = nn.LayerNorm(dim)
self.qkv = nn.Linear(dim, h)
self.proj = nn.Linear(self.dh, dim)
points = list(itertools.product(range(resolution[0]), range(resolution[1])))
N = len(points)
attention_offsets = {}
idxs = []
for p1 in points:
for p2 in points:
offset = (abs(p1[0] - p2[0]), abs(p1[1] - p2[1]))
if offset not in attention_offsets:
attention_offsets[offset] = len(attention_offsets)
idxs.append(attention_offsets[offset])
self.attention_biases = torch.nn.Parameter(torch.zeros(num_heads, len(attention_offsets)))
self.register_buffer("attention_bias_idxs", torch.LongTensor(idxs).view(N, N), persistent=False)
@torch.no_grad()
def train(self, mode: bool = True):
"""Set the module in training mode and handle the 'ab' attribute for cached attention biases."""
super().train(mode)
if mode and hasattr(self, "ab"):
del self.ab
else:
self.ab = self.attention_biases[:, self.attention_bias_idxs]
def forward(self, x: torch.Tensor) -> torch.Tensor:
"""Apply multi-head attention with spatial awareness and trainable attention biases."""
B, N, _ = x.shape # B, N, C
# Normalization
x = self.norm(x)
qkv = self.qkv(x)
# (B, N, num_heads, d)
q, k, v = qkv.view(B, N, self.num_heads, -1).split([self.key_dim, self.key_dim, self.d], dim=3)
# (B, num_heads, N, d)
q = q.permute(0, 2, 1, 3)
k = k.permute(0, 2, 1, 3)
v = v.permute(0, 2, 1, 3)
self.ab = self.ab.to(self.attention_biases.device)
attn = (q @ k.transpose(-2, -1)) * self.scale + (
self.attention_biases[:, self.attention_bias_idxs] if self.training else self.ab
)
attn = attn.softmax(dim=-1)
x = (attn @ v).transpose(1, 2).reshape(B, N, self.dh)
return self.proj(x)
class TinyViTBlock(nn.Module):
"""TinyViT Block that applies self-attention and a local convolution to the input.
This block is a key component of the TinyViT architecture, combining self-attention mechanisms with local
convolutions to process input features efficiently. It supports windowed attention for computational efficiency and
includes residual connections.
Attributes:
dim (int): The dimensionality of the input and output.
input_resolution (tuple[int, int]): Spatial resolution of the input feature map.
num_heads (int): Number of attention heads.
window_size (int): Size of the attention window.
mlp_ratio (float): Ratio of MLP hidden dimension to embedding dimension.
drop_path (nn.Module): Stochastic depth layer, identity function during inference.
attn (Attention): Self-attention module.
mlp (MLP): Multi-layer perceptron module.
local_conv (Conv2d_BN): Depth-wise local convolution layer.
Examples:
>>> input_tensor = torch.randn(1, 196, 192)
>>> block = TinyViTBlock(dim=192, input_resolution=(14, 14), num_heads=3)
>>> output = block(input_tensor)
>>> print(output.shape)
torch.Size([1, 196, 192])
"""
def __init__(
self,
dim: int,
input_resolution: tuple[int, int],
num_heads: int,
window_size: int = 7,
mlp_ratio: float = 4.0,
drop: float = 0.0,
drop_path: float = 0.0,
local_conv_size: int = 3,
activation=nn.GELU,
):
"""Initialize a TinyViT block with self-attention and local convolution.
This block is a key component of the TinyViT architecture, combining self-attention mechanisms with local
convolutions to process input features efficiently.
Args:
dim (int): Dimensionality of the input and output features.
input_resolution (tuple[int, int]): Spatial resolution of the input feature map (height, width).
num_heads (int): Number of attention heads.
window_size (int, optional): Size of the attention window. Must be greater than 0.
mlp_ratio (float, optional): Ratio of MLP hidden dimension to embedding dimension.
drop (float, optional): Dropout rate.
drop_path (float, optional): Stochastic depth rate.
local_conv_size (int, optional): Kernel size of the local convolution.
activation (nn.Module): Activation function for MLP.
"""
super().__init__()
self.dim = dim
self.input_resolution = input_resolution
self.num_heads = num_heads
assert window_size > 0, "window_size must be greater than 0"
self.window_size = window_size
self.mlp_ratio = mlp_ratio
# NOTE: `DropPath` is needed only for training.
# self.drop_path = DropPath(drop_path) if drop_path > 0. else nn.Identity()
self.drop_path = nn.Identity()
assert dim % num_heads == 0, "dim must be divisible by num_heads"
head_dim = dim // num_heads
window_resolution = (window_size, window_size)
self.attn = Attention(dim, head_dim, num_heads, attn_ratio=1, resolution=window_resolution)
mlp_hidden_dim = int(dim * mlp_ratio)
mlp_activation = activation
self.mlp = MLP(in_features=dim, hidden_features=mlp_hidden_dim, activation=mlp_activation, drop=drop)
pad = local_conv_size // 2
self.local_conv = Conv2d_BN(dim, dim, ks=local_conv_size, stride=1, pad=pad, groups=dim)
def forward(self, x: torch.Tensor) -> torch.Tensor:
"""Apply self-attention, local convolution, and MLP operations to the input tensor."""
h, w = self.input_resolution
b, hw, c = x.shape # batch, height*width, channels
assert hw == h * w, "input feature has wrong size"
res_x = x
if h == self.window_size and w == self.window_size:
x = self.attn(x)
else:
x = x.view(b, h, w, c)
pad_b = (self.window_size - h % self.window_size) % self.window_size
pad_r = (self.window_size - w % self.window_size) % self.window_size
padding = pad_b > 0 or pad_r > 0
if padding:
x = F.pad(x, (0, 0, 0, pad_r, 0, pad_b))
pH, pW = h + pad_b, w + pad_r
nH = pH // self.window_size
nW = pW // self.window_size
# Window partition
x = (
x.view(b, nH, self.window_size, nW, self.window_size, c)
.transpose(2, 3)
.reshape(b * nH * nW, self.window_size * self.window_size, c)
)
x = self.attn(x)
# Window reverse
x = x.view(b, nH, nW, self.window_size, self.window_size, c).transpose(2, 3).reshape(b, pH, pW, c)
if padding:
x = x[:, :h, :w].contiguous()
x = x.view(b, hw, c)
x = res_x + self.drop_path(x)
x = x.transpose(1, 2).reshape(b, c, h, w)
x = self.local_conv(x)
x = x.view(b, c, hw).transpose(1, 2)
return x + self.drop_path(self.mlp(x))
def extra_repr(self) -> str:
"""Return a string representation of the TinyViTBlock's parameters.
This method provides a formatted string containing key information about the TinyViTBlock, including its
dimension, input resolution, number of attention heads, window size, and MLP ratio.
Returns:
(str): A formatted string containing the block's parameters.
Examples:
>>> block = TinyViTBlock(dim=192, input_resolution=(14, 14), num_heads=3, window_size=7, mlp_ratio=4.0)
>>> print(block.extra_repr())
dim=192, input_resolution=(14, 14), num_heads=3, window_size=7, mlp_ratio=4.0
"""
return (
f"dim={self.dim}, input_resolution={self.input_resolution}, num_heads={self.num_heads}, "
f"window_size={self.window_size}, mlp_ratio={self.mlp_ratio}"
)
class BasicLayer(nn.Module):
"""A basic TinyViT layer for one stage in a TinyViT architecture.
This class represents a single layer in the TinyViT model, consisting of multiple TinyViT blocks and an optional
downsampling operation. It processes features at a specific resolution and dimensionality within the overall
architecture.
Attributes:
dim (int): The dimensionality of the input and output features.
input_resolution (tuple[int, int]): Spatial resolution of the input feature map.
depth (int): Number of TinyViT blocks in this layer.
use_checkpoint (bool): Whether to use gradient checkpointing to save memory.
blocks (nn.ModuleList): List of TinyViT blocks that make up this layer.
downsample (nn.Module | None): Downsample layer at the end of the layer, if specified.
Examples:
>>> input_tensor = torch.randn(1, 3136, 192)
>>> layer = BasicLayer(dim=192, input_resolution=(56, 56), depth=2, num_heads=3, window_size=7)
>>> output = layer(input_tensor)
>>> print(output.shape)
torch.Size([1, 784, 384])
"""
def __init__(
self,
dim: int,
input_resolution: tuple[int, int],
depth: int,
num_heads: int,
window_size: int,
mlp_ratio: float = 4.0,
drop: float = 0.0,
drop_path: float | list[float] = 0.0,
downsample: nn.Module | None = None,
use_checkpoint: bool = False,
local_conv_size: int = 3,
activation=nn.GELU,
out_dim: int | None = None,
):
"""Initialize a BasicLayer in the TinyViT architecture.
This layer consists of multiple TinyViT blocks and an optional downsampling operation. It is designed to process
feature maps at a specific resolution and dimensionality within the TinyViT model.
Args:
dim (int): Dimensionality of the input and output features.
input_resolution (tuple[int, int]): Spatial resolution of the input feature map (height, width).
depth (int): Number of TinyViT blocks in this layer.
num_heads (int): Number of attention heads in each TinyViT block.
window_size (int): Size of the local window for attention computation.
mlp_ratio (float, optional): Ratio of MLP hidden dimension to embedding dimension.
drop (float, optional): Dropout rate.
drop_path (float | list[float], optional): Stochastic depth rate. Can be a float or a list of floats for
each block.
downsample (nn.Module | None, optional): Downsampling layer at the end of the layer. None to skip
downsampling.
use_checkpoint (bool, optional): Whether to use gradient checkpointing to save memory.
local_conv_size (int, optional): Kernel size for the local convolution in each TinyViT block.
activation (nn.Module): Activation function used in the MLP.
out_dim (int | None, optional): Output dimension after downsampling. None means it will be the same as dim.
"""
super().__init__()
self.dim = dim
self.input_resolution = input_resolution
self.depth = depth
self.use_checkpoint = use_checkpoint
# Build blocks
self.blocks = nn.ModuleList(
[
TinyViTBlock(
dim=dim,
input_resolution=input_resolution,
num_heads=num_heads,
window_size=window_size,
mlp_ratio=mlp_ratio,
drop=drop,
drop_path=drop_path[i] if isinstance(drop_path, list) else drop_path,
local_conv_size=local_conv_size,
activation=activation,
)
for i in range(depth)
]
)
# Patch merging layer
self.downsample = (
None
if downsample is None
else downsample(input_resolution, dim=dim, out_dim=out_dim, activation=activation)
)
def forward(self, x: torch.Tensor) -> torch.Tensor:
"""Process input through TinyViT blocks and optional downsampling."""
for blk in self.blocks:
x = torch.utils.checkpoint(blk, x) if self.use_checkpoint else blk(x) # warn: checkpoint is slow import
return x if self.downsample is None else self.downsample(x)
def extra_repr(self) -> str:
"""Return a string with the layer's parameters for printing."""
return f"dim={self.dim}, input_resolution={self.input_resolution}, depth={self.depth}"
class TinyViT(nn.Module):
"""TinyViT: A compact vision transformer architecture for efficient image classification and feature extraction.
This class implements the TinyViT model, which combines elements of vision transformers and convolutional neural
networks for improved efficiency and performance on vision tasks. It features hierarchical processing with patch
embedding, multiple stages of attention and convolution blocks, and a feature refinement neck.
Attributes:
img_size (int): Input image size.
num_classes (int): Number of classification classes.
depths (tuple[int, int, int, int]): Number of blocks in each stage.
num_layers (int): Total number of layers in the network.
mlp_ratio (float): Ratio of MLP hidden dimension to embedding dimension.
patch_embed (PatchEmbed): Module for patch embedding.
patches_resolution (tuple[int, int]): Resolution of embedded patches.
layers (nn.ModuleList): List of network layers.
norm_head (nn.LayerNorm): Layer normalization for the classifier head.
head (nn.Linear): Linear layer for final classification.
neck (nn.Sequential): Neck module for feature refinement.
Examples:
>>> model = TinyViT(img_size=224, num_classes=1000)
>>> x = torch.randn(1, 3, 224, 224)
>>> features = model.forward_features(x)
>>> print(features.shape)
torch.Size([1, 256, 56, 56])
"""
def __init__(
self,
img_size: int = 224,
in_chans: int = 3,
num_classes: int = 1000,
embed_dims: tuple[int, int, int, int] = (96, 192, 384, 768),
depths: tuple[int, int, int, int] = (2, 2, 6, 2),
num_heads: tuple[int, int, int, int] = (3, 6, 12, 24),
window_sizes: tuple[int, int, int, int] = (7, 7, 14, 7),
mlp_ratio: float = 4.0,
drop_rate: float = 0.0,
drop_path_rate: float = 0.1,
use_checkpoint: bool = False,
mbconv_expand_ratio: float = 4.0,
local_conv_size: int = 3,
layer_lr_decay: float = 1.0,
):
"""Initialize the TinyViT model.
This constructor sets up the TinyViT architecture, including patch embedding, multiple layers of attention and
convolution blocks, and a classification head.
Args:
img_size (int, optional): Size of the input image.
in_chans (int, optional): Number of input channels.
num_classes (int, optional): Number of classes for classification.
embed_dims (tuple[int, int, int, int], optional): Embedding dimensions for each stage.
depths (tuple[int, int, int, int], optional): Number of blocks in each stage.
num_heads (tuple[int, int, int, int], optional): Number of attention heads in each stage.
window_sizes (tuple[int, int, int, int], optional): Window sizes for each stage.
mlp_ratio (float, optional): Ratio of MLP hidden dim to embedding dim.
drop_rate (float, optional): Dropout rate.
drop_path_rate (float, optional): Stochastic depth rate.
use_checkpoint (bool, optional): Whether to use checkpointing to save memory.
mbconv_expand_ratio (float, optional): Expansion ratio for MBConv layer.
local_conv_size (int, optional): Kernel size for local convolutions.
layer_lr_decay (float, optional): Layer-wise learning rate decay factor.
"""
super().__init__()
self.img_size = img_size
self.num_classes = num_classes
self.depths = depths
self.num_layers = len(depths)
self.mlp_ratio = mlp_ratio
activation = nn.GELU
self.patch_embed = PatchEmbed(
in_chans=in_chans, embed_dim=embed_dims[0], resolution=img_size, activation=activation
)
patches_resolution = self.patch_embed.patches_resolution
self.patches_resolution = patches_resolution
# Stochastic depth
dpr = [x.item() for x in torch.linspace(0, drop_path_rate, sum(depths))] # stochastic depth decay rule
# Build layers
self.layers = nn.ModuleList()
for i_layer in range(self.num_layers):
kwargs = dict(
dim=embed_dims[i_layer],
input_resolution=(
patches_resolution[0] // (2 ** (i_layer - 1 if i_layer == 3 else i_layer)),
patches_resolution[1] // (2 ** (i_layer - 1 if i_layer == 3 else i_layer)),
),
# input_resolution=(patches_resolution[0] // (2 ** i_layer),
# patches_resolution[1] // (2 ** i_layer)),
depth=depths[i_layer],
drop_path=dpr[sum(depths[:i_layer]) : sum(depths[: i_layer + 1])],
downsample=PatchMerging if (i_layer < self.num_layers - 1) else None,
use_checkpoint=use_checkpoint,
out_dim=embed_dims[min(i_layer + 1, len(embed_dims) - 1)],
activation=activation,
)
if i_layer == 0:
layer = ConvLayer(conv_expand_ratio=mbconv_expand_ratio, **kwargs)
else:
layer = BasicLayer(
num_heads=num_heads[i_layer],
window_size=window_sizes[i_layer],
mlp_ratio=self.mlp_ratio,
drop=drop_rate,
local_conv_size=local_conv_size,
**kwargs,
)
self.layers.append(layer)
# Classifier head
self.norm_head = nn.LayerNorm(embed_dims[-1])
self.head = nn.Linear(embed_dims[-1], num_classes) if num_classes > 0 else torch.nn.Identity()
# Init weights
self.apply(self._init_weights)
self.set_layer_lr_decay(layer_lr_decay)
self.neck = nn.Sequential(
nn.Conv2d(
embed_dims[-1],
256,
kernel_size=1,
bias=False,
),
LayerNorm2d(256),
nn.Conv2d(
256,
256,
kernel_size=3,
padding=1,
bias=False,
),
LayerNorm2d(256),
)
def set_layer_lr_decay(self, layer_lr_decay: float):
"""Set layer-wise learning rate decay for the TinyViT model based on depth."""
decay_rate = layer_lr_decay
# Layers -> blocks (depth)
depth = sum(self.depths)
lr_scales = [decay_rate ** (depth - i - 1) for i in range(depth)]
def _set_lr_scale(m, scale):
"""Set the learning rate scale for each layer in the model based on the layer's depth."""
for p in m.parameters():
p.lr_scale = scale
self.patch_embed.apply(lambda x: _set_lr_scale(x, lr_scales[0]))
i = 0
for layer in self.layers:
for block in layer.blocks:
block.apply(lambda x: _set_lr_scale(x, lr_scales[i]))
i += 1
if layer.downsample is not None:
layer.downsample.apply(lambda x: _set_lr_scale(x, lr_scales[i - 1]))
assert i == depth
for m in {self.norm_head, self.head}:
m.apply(lambda x: _set_lr_scale(x, lr_scales[-1]))
for k, p in self.named_parameters():
p.param_name = k
def _check_lr_scale(m):
"""Check if the learning rate scale attribute is present in module's parameters."""
for p in m.parameters():
assert hasattr(p, "lr_scale"), p.param_name
self.apply(_check_lr_scale)
@staticmethod
def _init_weights(m):
"""Initialize weights for linear and normalization layers in the TinyViT model."""
if isinstance(m, nn.Linear):
# NOTE: This initialization is needed only for training.
# trunc_normal_(m.weight, std=.02)
if m.bias is not None:
nn.init.constant_(m.bias, 0)
elif isinstance(m, nn.LayerNorm):
nn.init.constant_(m.bias, 0)
nn.init.constant_(m.weight, 1.0)
@torch.jit.ignore
def no_weight_decay_keywords(self):
"""Return a set of keywords for parameters that should not use weight decay."""
return {"attention_biases"}
def forward_features(self, x: torch.Tensor) -> torch.Tensor:
"""Process input through feature extraction layers, returning spatial features."""
x = self.patch_embed(x) # x input is (N, C, H, W)
x = self.layers[0](x)
start_i = 1
for i in range(start_i, len(self.layers)):
layer = self.layers[i]
x = layer(x)
batch, _, channel = x.shape
x = x.view(batch, self.patches_resolution[0] // 4, self.patches_resolution[1] // 4, channel)
x = x.permute(0, 3, 1, 2)
return self.neck(x)
def forward(self, x: torch.Tensor) -> torch.Tensor:
"""Perform the forward pass through the TinyViT model, extracting features from the input image."""
return self.forward_features(x)
def set_imgsz(self, imgsz: list[int] = [1024, 1024]):
"""Set image size to make model compatible with different image sizes."""
imgsz = [s // 4 for s in imgsz]
self.patches_resolution = imgsz
for i, layer in enumerate(self.layers):
input_resolution = (
imgsz[0] // (2 ** (i - 1 if i == 3 else i)),
imgsz[1] // (2 ** (i - 1 if i == 3 else i)),
)
layer.input_resolution = input_resolution
if layer.downsample is not None:
layer.downsample.input_resolution = input_resolution
if isinstance(layer, BasicLayer):
for b in layer.blocks:
b.input_resolution = input_resolution

View File

@@ -0,0 +1,344 @@
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
from __future__ import annotations
import math
import torch
from torch import Tensor, nn
from ultralytics.nn.modules import MLPBlock
class TwoWayTransformer(nn.Module):
"""A Two-Way Transformer module for simultaneous attention to image and query points.
This class implements a specialized transformer decoder that attends to an input image using queries with supplied
positional embeddings. It's useful for tasks like object detection, image segmentation, and point cloud processing.
Attributes:
depth (int): Number of layers in the transformer.
embedding_dim (int): Channel dimension for input embeddings.
num_heads (int): Number of heads for multihead attention.
mlp_dim (int): Internal channel dimension for the MLP block.
layers (nn.ModuleList): List of TwoWayAttentionBlock layers composing the transformer.
final_attn_token_to_image (Attention): Final attention layer from queries to image.
norm_final_attn (nn.LayerNorm): Layer normalization applied to final queries.
Methods:
forward: Process image and point embeddings through the transformer.
Examples:
>>> transformer = TwoWayTransformer(depth=6, embedding_dim=256, num_heads=8, mlp_dim=2048)
>>> image_embedding = torch.randn(1, 256, 32, 32)
>>> image_pe = torch.randn(1, 256, 32, 32)
>>> point_embedding = torch.randn(1, 100, 256)
>>> output_queries, output_image = transformer(image_embedding, image_pe, point_embedding)
>>> print(output_queries.shape, output_image.shape)
"""
def __init__(
self,
depth: int,
embedding_dim: int,
num_heads: int,
mlp_dim: int,
activation: type[nn.Module] = nn.ReLU,
attention_downsample_rate: int = 2,
) -> None:
"""Initialize a Two-Way Transformer for simultaneous attention to image and query points.
Args:
depth (int): Number of layers in the transformer.
embedding_dim (int): Channel dimension for input embeddings.
num_heads (int): Number of heads for multihead attention. Must divide embedding_dim.
mlp_dim (int): Internal channel dimension for the MLP block.
activation (Type[nn.Module], optional): Activation function to use in the MLP block.
attention_downsample_rate (int, optional): Downsampling rate for attention mechanism.
"""
super().__init__()
self.depth = depth
self.embedding_dim = embedding_dim
self.num_heads = num_heads
self.mlp_dim = mlp_dim
self.layers = nn.ModuleList()
for i in range(depth):
self.layers.append(
TwoWayAttentionBlock(
embedding_dim=embedding_dim,
num_heads=num_heads,
mlp_dim=mlp_dim,
activation=activation,
attention_downsample_rate=attention_downsample_rate,
skip_first_layer_pe=(i == 0),
)
)
self.final_attn_token_to_image = Attention(embedding_dim, num_heads, downsample_rate=attention_downsample_rate)
self.norm_final_attn = nn.LayerNorm(embedding_dim)
def forward(
self,
image_embedding: torch.Tensor,
image_pe: torch.Tensor,
point_embedding: torch.Tensor,
) -> tuple[torch.Tensor, torch.Tensor]:
"""Process image and point embeddings through the Two-Way Transformer.
Args:
image_embedding (torch.Tensor): Image to attend to, with shape (B, embedding_dim, H, W).
image_pe (torch.Tensor): Positional encoding to add to the image, with same shape as image_embedding.
point_embedding (torch.Tensor): Embedding to add to query points, with shape (B, N_points, embedding_dim).
Returns:
queries (torch.Tensor): Processed point embeddings with shape (B, N_points, embedding_dim).
keys (torch.Tensor): Processed image embeddings with shape (B, H*W, embedding_dim).
"""
# BxCxHxW -> BxHWxC == B x N_image_tokens x C
image_embedding = image_embedding.flatten(2).permute(0, 2, 1)
image_pe = image_pe.flatten(2).permute(0, 2, 1)
# Prepare queries
queries = point_embedding
keys = image_embedding
# Apply transformer blocks and final layernorm
for layer in self.layers:
queries, keys = layer(
queries=queries,
keys=keys,
query_pe=point_embedding,
key_pe=image_pe,
)
# Apply the final attention layer from the points to the image
q = queries + point_embedding
k = keys + image_pe
attn_out = self.final_attn_token_to_image(q=q, k=k, v=keys)
queries = queries + attn_out
queries = self.norm_final_attn(queries)
return queries, keys
class TwoWayAttentionBlock(nn.Module):
"""A two-way attention block for simultaneous attention to image and query points.
This class implements a specialized transformer block with four main layers: self-attention on sparse inputs,
cross-attention of sparse inputs to dense inputs, MLP block on sparse inputs, and cross-attention of dense inputs to
sparse inputs.
Attributes:
self_attn (Attention): Self-attention layer for queries.
norm1 (nn.LayerNorm): Layer normalization after self-attention.
cross_attn_token_to_image (Attention): Cross-attention layer from queries to keys.
norm2 (nn.LayerNorm): Layer normalization after token-to-image attention.
mlp (MLPBlock): MLP block for transforming query embeddings.
norm3 (nn.LayerNorm): Layer normalization after MLP block.
norm4 (nn.LayerNorm): Layer normalization after image-to-token attention.
cross_attn_image_to_token (Attention): Cross-attention layer from keys to queries.
skip_first_layer_pe (bool): Whether to skip positional encoding in the first layer.
Methods:
forward: Apply self-attention and cross-attention to queries and keys.
Examples:
>>> embedding_dim, num_heads = 256, 8
>>> block = TwoWayAttentionBlock(embedding_dim, num_heads)
>>> queries = torch.randn(1, 100, embedding_dim)
>>> keys = torch.randn(1, 1000, embedding_dim)
>>> query_pe = torch.randn(1, 100, embedding_dim)
>>> key_pe = torch.randn(1, 1000, embedding_dim)
>>> processed_queries, processed_keys = block(queries, keys, query_pe, key_pe)
"""
def __init__(
self,
embedding_dim: int,
num_heads: int,
mlp_dim: int = 2048,
activation: type[nn.Module] = nn.ReLU,
attention_downsample_rate: int = 2,
skip_first_layer_pe: bool = False,
) -> None:
"""Initialize a TwoWayAttentionBlock for simultaneous attention to image and query points.
This block implements a specialized transformer layer with four main components: self-attention on sparse
inputs, cross-attention of sparse inputs to dense inputs, MLP block on sparse inputs, and cross-attention of
dense inputs to sparse inputs.
Args:
embedding_dim (int): Channel dimension of the embeddings.
num_heads (int): Number of attention heads in the attention layers.
mlp_dim (int, optional): Hidden dimension of the MLP block.
activation (Type[nn.Module], optional): Activation function for the MLP block.
attention_downsample_rate (int, optional): Downsampling rate for the attention mechanism.
skip_first_layer_pe (bool, optional): Whether to skip positional encoding in the first layer.
"""
super().__init__()
self.self_attn = Attention(embedding_dim, num_heads)
self.norm1 = nn.LayerNorm(embedding_dim)
self.cross_attn_token_to_image = Attention(embedding_dim, num_heads, downsample_rate=attention_downsample_rate)
self.norm2 = nn.LayerNorm(embedding_dim)
self.mlp = MLPBlock(embedding_dim, mlp_dim, activation)
self.norm3 = nn.LayerNorm(embedding_dim)
self.norm4 = nn.LayerNorm(embedding_dim)
self.cross_attn_image_to_token = Attention(embedding_dim, num_heads, downsample_rate=attention_downsample_rate)
self.skip_first_layer_pe = skip_first_layer_pe
def forward(
self, queries: torch.Tensor, keys: torch.Tensor, query_pe: torch.Tensor, key_pe: torch.Tensor
) -> tuple[torch.Tensor, torch.Tensor]:
"""Apply two-way attention to process query and key embeddings in a transformer block.
Args:
queries (torch.Tensor): Query embeddings with shape (B, N_queries, embedding_dim).
keys (torch.Tensor): Key embeddings with shape (B, N_keys, embedding_dim).
query_pe (torch.Tensor): Positional encodings for queries with same shape as queries.
key_pe (torch.Tensor): Positional encodings for keys with same shape as keys.
Returns:
queries (torch.Tensor): Processed query embeddings with shape (B, N_queries, embedding_dim).
keys (torch.Tensor): Processed key embeddings with shape (B, N_keys, embedding_dim).
"""
# Self attention block
if self.skip_first_layer_pe:
queries = self.self_attn(q=queries, k=queries, v=queries)
else:
q = queries + query_pe
attn_out = self.self_attn(q=q, k=q, v=queries)
queries = queries + attn_out
queries = self.norm1(queries)
# Cross attention block, tokens attending to image embedding
q = queries + query_pe
k = keys + key_pe
attn_out = self.cross_attn_token_to_image(q=q, k=k, v=keys)
queries = queries + attn_out
queries = self.norm2(queries)
# MLP block
mlp_out = self.mlp(queries)
queries = queries + mlp_out
queries = self.norm3(queries)
# Cross attention block, image embedding attending to tokens
q = queries + query_pe
k = keys + key_pe
attn_out = self.cross_attn_image_to_token(q=k, k=q, v=queries)
keys = keys + attn_out
keys = self.norm4(keys)
return queries, keys
class Attention(nn.Module):
"""An attention layer with downscaling capability for embedding size after projection.
This class implements a multi-head attention mechanism with the option to downsample the internal dimension of
queries, keys, and values.
Attributes:
embedding_dim (int): Dimensionality of input embeddings.
kv_in_dim (int): Dimensionality of key and value inputs.
internal_dim (int): Internal dimension after downsampling.
num_heads (int): Number of attention heads.
q_proj (nn.Linear): Linear projection for queries.
k_proj (nn.Linear): Linear projection for keys.
v_proj (nn.Linear): Linear projection for values.
out_proj (nn.Linear): Linear projection for output.
Methods:
_separate_heads: Separate input tensor into attention heads.
_recombine_heads: Recombine separated attention heads.
forward: Compute attention output for given query, key, and value tensors.
Examples:
>>> attn = Attention(embedding_dim=256, num_heads=8, downsample_rate=2)
>>> q = torch.randn(1, 100, 256)
>>> k = v = torch.randn(1, 50, 256)
>>> output = attn(q, k, v)
>>> print(output.shape)
torch.Size([1, 100, 256])
"""
def __init__(
self,
embedding_dim: int,
num_heads: int,
downsample_rate: int = 1,
kv_in_dim: int | None = None,
) -> None:
"""Initialize the Attention module with specified dimensions and settings.
Args:
embedding_dim (int): Dimensionality of input embeddings.
num_heads (int): Number of attention heads.
downsample_rate (int, optional): Factor by which internal dimensions are downsampled.
kv_in_dim (int | None, optional): Dimensionality of key and value inputs. If None, uses embedding_dim.
Raises:
AssertionError: If num_heads does not evenly divide the internal dim (embedding_dim / downsample_rate).
"""
super().__init__()
self.embedding_dim = embedding_dim
self.kv_in_dim = kv_in_dim if kv_in_dim is not None else embedding_dim
self.internal_dim = embedding_dim // downsample_rate
self.num_heads = num_heads
assert self.internal_dim % num_heads == 0, "num_heads must divide embedding_dim."
self.q_proj = nn.Linear(embedding_dim, self.internal_dim)
self.k_proj = nn.Linear(self.kv_in_dim, self.internal_dim)
self.v_proj = nn.Linear(self.kv_in_dim, self.internal_dim)
self.out_proj = nn.Linear(self.internal_dim, embedding_dim)
@staticmethod
def _separate_heads(x: torch.Tensor, num_heads: int) -> torch.Tensor:
"""Separate the input tensor into the specified number of attention heads."""
b, n, c = x.shape
x = x.reshape(b, n, num_heads, c // num_heads)
return x.transpose(1, 2) # B x N_heads x N_tokens x C_per_head
@staticmethod
def _recombine_heads(x: Tensor) -> Tensor:
"""Recombine separated attention heads into a single tensor."""
b, n_heads, n_tokens, c_per_head = x.shape
x = x.transpose(1, 2)
return x.reshape(b, n_tokens, n_heads * c_per_head) # B x N_tokens x C
def forward(self, q: torch.Tensor, k: torch.Tensor, v: torch.Tensor) -> torch.Tensor:
"""Apply multi-head attention to query, key, and value tensors with optional downsampling.
Args:
q (torch.Tensor): Query tensor with shape (B, N_q, embedding_dim).
k (torch.Tensor): Key tensor with shape (B, N_k, embedding_dim).
v (torch.Tensor): Value tensor with shape (B, N_k, embedding_dim).
Returns:
(torch.Tensor): Output tensor after attention with shape (B, N_q, embedding_dim).
"""
# Input projections
q = self.q_proj(q)
k = self.k_proj(k)
v = self.v_proj(v)
# Separate into heads
q = self._separate_heads(q, self.num_heads)
k = self._separate_heads(k, self.num_heads)
v = self._separate_heads(v, self.num_heads)
# Attention
_, _, _, c_per_head = q.shape
attn = q @ k.permute(0, 1, 3, 2) # B x N_heads x N_tokens x N_tokens
attn = attn / math.sqrt(c_per_head)
attn = torch.softmax(attn, dim=-1)
# Get output
out = attn @ v
out = self._recombine_heads(out)
return self.out_proj(out)

View File

@@ -0,0 +1,512 @@
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
from __future__ import annotations
import math
from typing import Any
import torch
import torch.nn.functional as F
def select_closest_cond_frames(frame_idx: int, cond_frame_outputs: dict[int, Any], max_cond_frame_num: int):
"""Select the closest conditioning frames to a given frame index.
Args:
frame_idx (int): Current frame index.
cond_frame_outputs (dict[int, Any]): Dictionary of conditioning frame outputs keyed by frame indices.
max_cond_frame_num (int): Maximum number of conditioning frames to select.
Returns:
selected_outputs (dict[int, Any]): Selected items from cond_frame_outputs.
unselected_outputs (dict[int, Any]): Items not selected from cond_frame_outputs.
Examples:
>>> frame_idx = 5
>>> cond_frame_outputs = {1: "a", 3: "b", 7: "c", 9: "d"}
>>> max_cond_frame_num = 2
>>> selected, unselected = select_closest_cond_frames(frame_idx, cond_frame_outputs, max_cond_frame_num)
>>> print(selected)
{3: 'b', 7: 'c'}
>>> print(unselected)
{1: 'a', 9: 'd'}
"""
if max_cond_frame_num == -1 or len(cond_frame_outputs) <= max_cond_frame_num:
selected_outputs = cond_frame_outputs
unselected_outputs = {}
else:
assert max_cond_frame_num >= 2, "we should allow using 2+ conditioning frames"
selected_outputs = {}
# The closest conditioning frame before `frame_idx` (if any)
idx_before = max((t for t in cond_frame_outputs if t < frame_idx), default=None)
if idx_before is not None:
selected_outputs[idx_before] = cond_frame_outputs[idx_before]
# The closest conditioning frame after `frame_idx` (if any)
idx_after = min((t for t in cond_frame_outputs if t >= frame_idx), default=None)
if idx_after is not None:
selected_outputs[idx_after] = cond_frame_outputs[idx_after]
# Add other temporally closest conditioning frames until reaching a total
# of `max_cond_frame_num` conditioning frames.
num_remain = max_cond_frame_num - len(selected_outputs)
inds_remain = sorted(
(t for t in cond_frame_outputs if t not in selected_outputs),
key=lambda x: abs(x - frame_idx),
)[:num_remain]
selected_outputs.update((t, cond_frame_outputs[t]) for t in inds_remain)
unselected_outputs = {t: v for t, v in cond_frame_outputs.items() if t not in selected_outputs}
return selected_outputs, unselected_outputs
def get_1d_sine_pe(pos_inds: torch.Tensor, dim: int, temperature: float = 10000):
"""Generate 1D sinusoidal positional embeddings for given positions and dimensions.
Args:
pos_inds (torch.Tensor): Position indices for which to generate embeddings.
dim (int): Dimension of the positional embeddings. Should be an even number.
temperature (float, optional): Scaling factor for the frequency of the sinusoidal functions.
Returns:
(torch.Tensor): Sinusoidal positional embeddings with shape (pos_inds.shape, dim).
Examples:
>>> pos = torch.tensor([0, 1, 2, 3])
>>> embeddings = get_1d_sine_pe(pos, 128)
>>> embeddings.shape
torch.Size([4, 128])
"""
pe_dim = dim // 2
dim_t = torch.arange(pe_dim, dtype=pos_inds.dtype, device=pos_inds.device)
dim_t = temperature ** (2 * (dim_t // 2) / pe_dim)
pos_embed = pos_inds.unsqueeze(-1) / dim_t
pos_embed = torch.cat([pos_embed.sin(), pos_embed.cos()], dim=-1)
return pos_embed
def init_t_xy(end_x: int, end_y: int, scale: float = 1.0, offset: int = 0):
"""Initialize 1D and 2D coordinate tensors for a grid of specified dimensions.
This function creates coordinate tensors for a grid with dimensions end_x × end_y. It generates a linear index
tensor and corresponding x and y coordinate tensors.
Args:
end_x (int): Width of the grid (number of columns).
end_y (int): Height of the grid (number of rows).
scale (float): Scaling factor to apply to the coordinates.
offset (int): Offset to add to the coordinates.
Returns:
t_x (torch.Tensor): X-coordinates for each position, with shape (end_x * end_y).
t_y (torch.Tensor): Y-coordinates for each position, with shape (end_x * end_y).
Examples:
>>> t_x, t_y = init_t_xy(3, 2)
>>> print(t_x)
tensor([0., 1., 2., 0., 1., 2.])
>>> print(t_y)
tensor([0., 0., 0., 1., 1., 1.])
"""
t = torch.arange(end_x * end_y, dtype=torch.float32)
t_x = (t % end_x).float()
t_y = torch.div(t, end_x, rounding_mode="floor").float()
return t_x * scale + offset, t_y * scale + offset
def compute_axial_cis(dim: int, end_x: int, end_y: int, theta: float = 10000.0, scale_pos: float = 1.0):
"""Compute axial complex exponential positional encodings for 2D spatial positions in a grid.
This function generates complex exponential positional encodings for a 2D grid of spatial positions, using separate
frequency components for the x and y dimensions.
Args:
dim (int): Dimension of the positional encoding.
end_x (int): Width of the 2D grid.
end_y (int): Height of the 2D grid.
theta (float, optional): Scaling factor for frequency computation.
scale_pos (float, optional): Scaling factor for position coordinates.
Returns:
(torch.Tensor): Complex exponential positional encodings with shape (end_x*end_y, dim//2).
Examples:
>>> dim, end_x, end_y = 128, 8, 8
>>> freqs_cis = compute_axial_cis(dim, end_x, end_y)
>>> freqs_cis.shape
torch.Size([64, 64])
"""
freqs_x = 1.0 / (theta ** (torch.arange(0, dim, 4)[: (dim // 4)].float() / dim))
freqs_y = 1.0 / (theta ** (torch.arange(0, dim, 4)[: (dim // 4)].float() / dim))
t_x, t_y = init_t_xy(end_x, end_y, scale=scale_pos)
freqs_x = torch.outer(t_x, freqs_x)
freqs_y = torch.outer(t_y, freqs_y)
freqs_cis_x = torch.polar(torch.ones_like(freqs_x), freqs_x)
freqs_cis_y = torch.polar(torch.ones_like(freqs_y), freqs_y)
return torch.cat([freqs_cis_x, freqs_cis_y], dim=-1)
def reshape_for_broadcast(freqs_cis: torch.Tensor, x: torch.Tensor):
"""Reshape frequency tensor for broadcasting with input tensor.
Reshapes a frequency tensor to ensure dimensional compatibility for broadcasting with an input tensor. This function
is typically used in positional encoding operations.
Args:
freqs_cis (torch.Tensor): Frequency tensor with shape matching the last two dimensions of x.
x (torch.Tensor): Input tensor to broadcast with.
Returns:
(torch.Tensor): Reshaped frequency tensor ready for broadcasting with the input tensor.
Raises:
AssertionError: If the shape of freqs_cis doesn't match the last two dimensions of x.
"""
ndim = x.ndim
assert ndim >= 2
assert freqs_cis.shape == (x.shape[-2], x.shape[-1])
shape = [d if i >= ndim - 2 else 1 for i, d in enumerate(x.shape)]
return freqs_cis.view(*shape)
def apply_rotary_enc(
xq: torch.Tensor,
xk: torch.Tensor,
freqs_cis: torch.Tensor,
repeat_freqs_k: bool = False,
):
"""Apply rotary positional encoding to query and key tensors.
This function applies rotary positional encoding (RoPE) to query and key tensors using complex-valued frequency
components. RoPE is a technique that injects relative position information into self-attention mechanisms.
Args:
xq (torch.Tensor): Query tensor to encode with positional information.
xk (torch.Tensor): Key tensor to encode with positional information.
freqs_cis (torch.Tensor): Complex-valued frequency components for rotary encoding with shape matching the last
two dimensions of xq.
repeat_freqs_k (bool, optional): Whether to repeat frequency components along sequence length dimension to match
key sequence length.
Returns:
xq_out (torch.Tensor): Query tensor with rotary positional encoding applied.
xk_out (torch.Tensor): Key tensor with rotary positional encoding applied, or original xk if xk is empty.
Examples:
>>> import torch
>>> xq = torch.randn(2, 8, 16, 64) # [batch, heads, seq_len, dim]
>>> xk = torch.randn(2, 8, 16, 64)
>>> freqs_cis = compute_axial_cis(64, 4, 4) # For a 4x4 spatial grid with dim=64
>>> q_encoded, k_encoded = apply_rotary_enc(xq, xk, freqs_cis)
"""
xq_ = torch.view_as_complex(xq.float().reshape(*xq.shape[:-1], -1, 2))
xk_ = torch.view_as_complex(xk.float().reshape(*xk.shape[:-1], -1, 2)) if xk.shape[-2] != 0 else None
freqs_cis = reshape_for_broadcast(freqs_cis, xq_)
xq_out = torch.view_as_real(xq_ * freqs_cis).flatten(3)
if xk_ is None:
# No keys to rotate, due to dropout
return xq_out.type_as(xq).to(xq.device), xk
# Repeat freqs along seq_len dim to match k seq_len
if repeat_freqs_k and (r := xk_.shape[-2] // xq_.shape[-2]) > 1:
# MPS doesn't support repeat on complex tensors, decompose to real representation
if freqs_cis.device.type == "mps":
freqs_cis = torch.view_as_real(freqs_cis)
freqs_cis = freqs_cis.repeat(*([1] * (freqs_cis.ndim - 3)), r, 1, 1)
freqs_cis = torch.view_as_complex(freqs_cis.contiguous())
else:
freqs_cis = freqs_cis.repeat(*([1] * (freqs_cis.ndim - 2)), r, 1)
xk_out = torch.view_as_real(xk_ * freqs_cis).flatten(3)
return xq_out.type_as(xq).to(xq.device), xk_out.type_as(xk).to(xk.device)
def window_partition(x: torch.Tensor, window_size: int):
"""Partition input tensor into non-overlapping windows with padding if needed.
Args:
x (torch.Tensor): Input tensor with shape (B, H, W, C).
window_size (int): Size of each window.
Returns:
windows (torch.Tensor): Partitioned windows with shape (B * num_windows, window_size, window_size, C).
padded_h_w (tuple[int, int]): Padded height and width before partition.
Examples:
>>> x = torch.randn(1, 16, 16, 3)
>>> windows, (Hp, Wp) = window_partition(x, window_size=4)
>>> print(windows.shape, Hp, Wp)
torch.Size([16, 4, 4, 3]) 16 16
"""
B, H, W, C = x.shape
pad_h = (window_size - H % window_size) % window_size
pad_w = (window_size - W % window_size) % window_size
if pad_h > 0 or pad_w > 0:
x = F.pad(x, (0, 0, 0, pad_w, 0, pad_h))
Hp, Wp = H + pad_h, W + pad_w
x = x.view(B, Hp // window_size, window_size, Wp // window_size, window_size, C)
windows = x.permute(0, 1, 3, 2, 4, 5).contiguous().view(-1, window_size, window_size, C)
return windows, (Hp, Wp)
def window_unpartition(windows: torch.Tensor, window_size: int, pad_hw: tuple[int, int], hw: tuple[int, int]):
"""Unpartition windowed sequences into original sequences and remove padding.
This function reverses the windowing process, reconstructing the original input from windowed segments and removing
any padding that was added during the windowing process.
Args:
windows (torch.Tensor): Input tensor of windowed sequences with shape (B * num_windows, window_size,
window_size, C), where B is the batch size, num_windows is the number of windows, window_size is the size of
each window, and C is the number of channels.
window_size (int): Size of each window.
pad_hw (tuple[int, int]): Padded height and width (Hp, Wp) of the input before windowing.
hw (tuple[int, int]): Original height and width (H, W) of the input before padding and windowing.
Returns:
(torch.Tensor): Unpartitioned sequences with shape (B, H, W, C), where B is the batch size, H and W are the
original height and width, and C is the number of channels.
Examples:
>>> windows = torch.rand(32, 8, 8, 64) # 32 windows of size 8x8 with 64 channels
>>> pad_hw = (16, 16) # Padded height and width
>>> hw = (15, 14) # Original height and width
>>> x = window_unpartition(windows, window_size=8, pad_hw=pad_hw, hw=hw)
>>> print(x.shape)
torch.Size([1, 15, 14, 64])
"""
Hp, Wp = pad_hw
H, W = hw
B = windows.shape[0] // (Hp * Wp // window_size // window_size)
x = windows.view(B, Hp // window_size, Wp // window_size, window_size, window_size, -1)
x = x.permute(0, 1, 3, 2, 4, 5).contiguous().view(B, Hp, Wp, -1)
if Hp > H or Wp > W:
x = x[:, :H, :W, :].contiguous()
return x
def get_rel_pos(q_size: int, k_size: int, rel_pos: torch.Tensor) -> torch.Tensor:
"""Extract relative positional embeddings based on query and key sizes.
Args:
q_size (int): Size of the query.
k_size (int): Size of the key.
rel_pos (torch.Tensor): Relative position embeddings with shape (L, C), where L is the maximum relative distance
and C is the embedding dimension.
Returns:
(torch.Tensor): Extracted positional embeddings according to relative positions, with shape (q_size, k_size, C).
Examples:
>>> q_size, k_size = 8, 16
>>> rel_pos = torch.randn(31, 64) # 31 = 2 * max(8, 16) - 1
>>> extracted_pos = get_rel_pos(q_size, k_size, rel_pos)
>>> print(extracted_pos.shape)
torch.Size([8, 16, 64])
"""
max_rel_dist = int(2 * max(q_size, k_size) - 1)
# Interpolate rel pos if needed.
if rel_pos.shape[0] != max_rel_dist:
# Interpolate rel pos.
rel_pos_resized = F.interpolate(
rel_pos.reshape(1, rel_pos.shape[0], -1).permute(0, 2, 1),
size=max_rel_dist,
mode="linear",
)
rel_pos_resized = rel_pos_resized.reshape(-1, max_rel_dist).permute(1, 0)
else:
rel_pos_resized = rel_pos
# Scale the coords with short length if shapes for q and k are different.
q_coords = torch.arange(q_size)[:, None] * max(k_size / q_size, 1.0)
k_coords = torch.arange(k_size)[None, :] * max(q_size / k_size, 1.0)
relative_coords = (q_coords - k_coords) + (k_size - 1) * max(q_size / k_size, 1.0)
return rel_pos_resized[relative_coords.long()]
def add_decomposed_rel_pos(
attn: torch.Tensor,
q: torch.Tensor,
rel_pos_h: torch.Tensor,
rel_pos_w: torch.Tensor,
q_size: tuple[int, int],
k_size: tuple[int, int],
) -> torch.Tensor:
"""Add decomposed Relative Positional Embeddings to the attention map.
This function calculates and applies decomposed Relative Positional Embeddings as described in the MVITv2
paper. It enhances the attention mechanism by incorporating spatial relationships between query and key
positions.
Args:
attn (torch.Tensor): Attention map with shape (B, q_h * q_w, k_h * k_w).
q (torch.Tensor): Query tensor in the attention layer with shape (B, q_h * q_w, C).
rel_pos_h (torch.Tensor): Relative position embeddings for height axis with shape (Lh, C).
rel_pos_w (torch.Tensor): Relative position embeddings for width axis with shape (Lw, C).
q_size (tuple[int, int]): Spatial sequence size of query q as (q_h, q_w).
k_size (tuple[int, int]): Spatial sequence size of key k as (k_h, k_w).
Returns:
(torch.Tensor): Updated attention map with added relative positional embeddings, shape (B, q_h * q_w, k_h *
k_w).
Examples:
>>> B, C, q_h, q_w, k_h, k_w = 1, 64, 8, 8, 8, 8
>>> attn = torch.rand(B, q_h * q_w, k_h * k_w)
>>> q = torch.rand(B, q_h * q_w, C)
>>> rel_pos_h = torch.rand(2 * max(q_h, k_h) - 1, C)
>>> rel_pos_w = torch.rand(2 * max(q_w, k_w) - 1, C)
>>> q_size, k_size = (q_h, q_w), (k_h, k_w)
>>> updated_attn = add_decomposed_rel_pos(attn, q, rel_pos_h, rel_pos_w, q_size, k_size)
>>> print(updated_attn.shape)
torch.Size([1, 64, 64])
References:
https://github.com/facebookresearch/mvit/blob/main/mvit/models/attention.py
"""
q_h, q_w = q_size
k_h, k_w = k_size
Rh = get_rel_pos(q_h, k_h, rel_pos_h)
Rw = get_rel_pos(q_w, k_w, rel_pos_w)
B, _, dim = q.shape
r_q = q.reshape(B, q_h, q_w, dim)
rel_h = torch.einsum("bhwc,hkc->bhwk", r_q, Rh)
rel_w = torch.einsum("bhwc,wkc->bhwk", r_q, Rw)
attn = (attn.view(B, q_h, q_w, k_h, k_w) + rel_h[:, :, :, :, None] + rel_w[:, :, :, None, :]).view(
B, q_h * q_w, k_h * k_w
)
return attn
def get_abs_pos(
abs_pos: torch.Tensor,
has_cls_token: bool,
hw: tuple[int, int],
retain_cls_token: bool = False,
tiling: bool = False,
) -> torch.Tensor:
"""Calculate absolute positional embeddings. If needed, resize embeddings and remove cls_token dimension for the
original embeddings.
Args:
abs_pos (Tensor): absolute positional embeddings with (1, num_position, C).
has_cls_token (bool): If true, has 1 embedding in abs_pos for cls token.
hw (Tuple): size of input image tokens.
retain_cls_token: whether to retain the cls_token
tiling: whether to tile the embeddings, *instead* of interpolation (a la abs_win)
Returns:
Absolute positional embeddings after processing with shape (1, H, W, C),: if retain_cls_token is False,
otherwise (1, 1+H*W, C).
"""
if retain_cls_token:
assert has_cls_token
h, w = hw
if has_cls_token:
cls_pos = abs_pos[:, :1]
abs_pos = abs_pos[:, 1:]
xy_num = abs_pos.shape[1]
size = int(math.sqrt(xy_num))
assert size * size == xy_num
if size != h or size != w:
new_abs_pos = abs_pos.reshape(1, size, size, -1).permute(0, 3, 1, 2)
if tiling:
new_abs_pos = new_abs_pos.tile([1, 1] + [x // y + 1 for x, y in zip((h, w), new_abs_pos.shape[2:])])[
:, :, :h, :w
]
else:
new_abs_pos = F.interpolate(
new_abs_pos,
size=(h, w),
mode="bicubic",
align_corners=False,
)
if not retain_cls_token:
return new_abs_pos.permute(0, 2, 3, 1)
else:
# add cls_token back, flatten spatial dims
assert has_cls_token
return torch.cat(
[cls_pos, new_abs_pos.permute(0, 2, 3, 1).reshape(1, h * w, -1)],
dim=1,
)
else:
if not retain_cls_token:
return abs_pos.reshape(1, h, w, -1)
else:
assert has_cls_token
return torch.cat([cls_pos, abs_pos], dim=1)
def concat_rel_pos(
q: torch.Tensor,
k: torch.Tensor,
q_hw: tuple[int, int],
k_hw: tuple[int, int],
rel_pos_h: torch.Tensor,
rel_pos_w: torch.Tensor,
rescale: bool = False,
relative_coords: torch.Tensor = None,
) -> tuple[torch.Tensor, torch.Tensor]:
"""Concatenate rel pos coeffs to the q & k tensors, so that qk^T is now effectively including rel pos biases.
Args:
q (torch.Tensor): q tensor with shape (B, L_q, C).
k (torch.Tensor): k tensor with shape (B, L_k, C).
q_hw: These are spatial size of q tensors.
k_hw: These are spatial size of k tensors.
rel_pos_h: These are relative pos embeddings/params of height.
rel_pos_w: These are relative pos embeddings/params of width.
rescale (bool): whether to rescale. e.g. for use when using sdpa, pytorch will scale by the wrong factor due to
the concat.
relative_coords (torch.Tensor, optional): Precomputed relative coords index tensor.
Returns:
q, k: But, padded so that qk^T accounts for rel pos biases.
"""
q_h, q_w = q_hw
k_h, k_w = k_hw
assert (q_h == q_w) and (k_h == k_w), "only square inputs supported"
if relative_coords is not None:
Rh = rel_pos_h[relative_coords]
Rw = rel_pos_w[relative_coords]
else:
Rh = get_rel_pos(q_h, k_h, rel_pos_h)
Rw = get_rel_pos(q_w, k_w, rel_pos_w)
B, _, dim = q.shape
r_q = q.reshape(B, q_h, q_w, dim)
old_scale = dim**0.5
new_scale = (dim + k_h + k_w) ** 0.5 if rescale else old_scale # for sdpa
# attn will be divided by new_scale, but we want to divide q by old_scale
scale_ratio = new_scale / old_scale
rel_h = torch.einsum("bhwc,hkc->bhwk", r_q, Rh) * new_scale # (B, q_h, q_w, k_h)
rel_w = torch.einsum("bhwc,wkc->bhwk", r_q, Rw) * new_scale # (B, q_h, q_w, k_w)
eye_h = torch.eye(k_h, dtype=q.dtype, device=q.device)
eye_w = torch.eye(k_w, dtype=q.dtype, device=q.device)
eye_h = eye_h.view(1, k_h, 1, k_h).expand([B, k_h, k_w, k_h])
eye_w = eye_w.view(1, 1, k_w, k_w).expand([B, k_h, k_w, k_w])
q = torch.cat([r_q * scale_ratio, rel_h, rel_w], dim=-1).view(B, q_h * q_w, -1)
k = torch.cat([k.view(B, k_h, k_w, -1), eye_h, eye_w], dim=-1).view(B, k_h * k_w, -1)
return q, k

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,3 @@
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved

View File

@@ -0,0 +1,546 @@
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
"""
Transformer decoder.
Inspired from Pytorch's version, adds the pre-norm variant.
"""
from __future__ import annotations
import numpy as np
import torch
from torch import nn
from torchvision.ops.roi_align import RoIAlign
from ultralytics.nn.modules.transformer import MLP
from ultralytics.nn.modules.utils import _get_clones, inverse_sigmoid
from ultralytics.utils.ops import xywh2xyxy
from .model_misc import gen_sineembed_for_position
class TransformerDecoderLayer(nn.Module):
"""TransformerDecoderLayer is made up of self-attn, cross-attn, and feedforward network (FFN)."""
def __init__(
self,
d_model: int,
dim_feedforward: int,
dropout: float,
cross_attention: nn.Module,
n_heads: int,
use_text_cross_attention: bool = False,
):
"""Initialize the TransformerDecoderLayer."""
super().__init__()
# cross attention
self.cross_attn = cross_attention
self.dropout1 = nn.Dropout(dropout) if dropout > 0 else nn.Identity()
self.norm1 = nn.LayerNorm(d_model)
# cross attention text
self.use_text_cross_attention = use_text_cross_attention
if use_text_cross_attention:
self.ca_text = nn.MultiheadAttention(d_model, n_heads, dropout=dropout)
self.catext_dropout = nn.Dropout(dropout) if dropout > 0 else nn.Identity()
self.catext_norm = nn.LayerNorm(d_model)
# self attention
self.self_attn = nn.MultiheadAttention(d_model, n_heads, dropout=dropout)
self.dropout2 = nn.Dropout(dropout) if dropout > 0 else nn.Identity()
self.norm2 = nn.LayerNorm(d_model)
# ffn
self.linear1 = nn.Linear(d_model, dim_feedforward)
self.activation = nn.ReLU()
self.dropout3 = nn.Dropout(dropout) if dropout > 0 else nn.Identity()
self.linear2 = nn.Linear(dim_feedforward, d_model)
self.dropout4 = nn.Dropout(dropout) if dropout > 0 else nn.Identity()
self.norm3 = nn.LayerNorm(d_model)
@staticmethod
def with_pos_embed(tensor, pos):
"""Add positional embedding to the tensor."""
return tensor if pos is None else tensor + pos
def forward_ffn(self, tgt):
"""Feedforward network forward pass."""
tgt2 = self.linear2(self.dropout3(self.activation(self.linear1(tgt))))
tgt = tgt + self.dropout4(tgt2)
tgt = self.norm3(tgt)
return tgt
def forward(
self,
# for tgt
tgt: torch.Tensor, # nq, bs, d_model
tgt_query_pos: torch.Tensor = None, # pos for query. MLP(Sine(pos))
memory_text: torch.Tensor = None, # num_token, bs, d_model
text_attention_mask: torch.Tensor = None, # bs, num_token
# for memory
memory: torch.Tensor = None, # hw, bs, d_model
memory_key_padding_mask: torch.Tensor = None,
memory_pos: torch.Tensor = None, # pos for memory
# sa
self_attn_mask: torch.Tensor = None, # mask used for self-attention
cross_attn_mask: torch.Tensor = None, # mask used for cross-attention
# dac
dac=False,
dac_use_selfatt_ln=True,
presence_token=None,
# skip inside deformable attn
**kwargs, # additional kwargs for compatibility
):
"""Input: - tgt/tgt_query_pos: nq, bs, d_model. -."""
# self attention
tgt, tgt_query_pos = self._apply_self_attention(
tgt, tgt_query_pos, dac, dac_use_selfatt_ln, presence_token, self_attn_mask
)
if self.use_text_cross_attention:
tgt2 = self.ca_text(
self.with_pos_embed(tgt, tgt_query_pos),
memory_text.to(tgt.dtype),
memory_text.to(tgt.dtype),
key_padding_mask=text_attention_mask,
)[0]
tgt = tgt + self.catext_dropout(tgt2)
tgt = self.catext_norm(tgt)
if presence_token is not None:
presence_token_mask = torch.zeros_like(cross_attn_mask[:, :1, :])
cross_attn_mask = torch.cat([presence_token_mask, cross_attn_mask], dim=1) # (bs*nheads, 1+nq, hw)
# Cross attention to image
tgt2 = self.cross_attn(
query=self.with_pos_embed(tgt, tgt_query_pos),
key=self.with_pos_embed(memory, memory_pos),
value=memory,
attn_mask=cross_attn_mask,
key_padding_mask=(memory_key_padding_mask.transpose(0, 1) if memory_key_padding_mask is not None else None),
need_weights=False,
)[0]
tgt = tgt + self.dropout1(tgt2)
tgt = self.norm1(tgt)
# ffn
tgt = self.forward_ffn(tgt.to(memory.dtype))
presence_token_out = None
if presence_token is not None:
presence_token_out = tgt[:1]
tgt = tgt[1:]
return tgt, presence_token_out
def _apply_self_attention(self, tgt, tgt_query_pos, dac, dac_use_selfatt_ln, presence_token, self_attn_mask):
"""Apply self-attention with optional DAC splitting."""
if self.self_attn is None:
return tgt
if dac:
# Split queries for DAC (detect-and-classify)
assert tgt.shape[0] % 2 == 0, "DAC requires even number of queries"
num_o2o_queries = tgt.shape[0] // 2
tgt_o2o = tgt[:num_o2o_queries]
tgt_query_pos_o2o = tgt_query_pos[:num_o2o_queries]
tgt_o2m = tgt[num_o2o_queries:]
else:
tgt_o2o = tgt
tgt_query_pos_o2o = tgt_query_pos
# Handle presence token
if presence_token is not None:
tgt_o2o = torch.cat([presence_token, tgt_o2o], dim=0)
tgt_query_pos_o2o = torch.cat([torch.zeros_like(presence_token), tgt_query_pos_o2o], dim=0).to(
tgt_o2o.dtype
)
tgt_query_pos = torch.cat([torch.zeros_like(presence_token), tgt_query_pos], dim=0)
# Self-attention
q = k = self.with_pos_embed(tgt_o2o, tgt_query_pos_o2o)
tgt2 = self.self_attn(q, k, tgt_o2o, attn_mask=self_attn_mask)[0].to(tgt.dtype)
tgt_o2o = tgt_o2o + self.dropout2(tgt2)
# Recombine and normalize
if dac:
if not dac_use_selfatt_ln:
tgt_o2o = self.norm2(tgt_o2o)
tgt = torch.cat((tgt_o2o, tgt_o2m), dim=0)
if dac_use_selfatt_ln:
tgt = self.norm2(tgt)
else:
tgt = tgt_o2o
tgt = self.norm2(tgt)
return tgt, tgt_query_pos
class TransformerDecoder(nn.Module):
"""Transformer Decoder consisting of multiple layers."""
def __init__(
self,
d_model: int,
frozen: bool,
interaction_layer,
layer,
num_layers: int,
num_queries: int,
return_intermediate: bool,
box_refine: bool = False,
num_o2m_queries: int = 0,
dac: bool = False,
boxRPB: str = "none",
# Experimental: An object query for SAM 2 tasks
instance_query: bool = False,
# Defines the number of additional instance queries,
# 1 or 4 are the most likely for single vs multi mask support
num_instances: int = 1, # Irrelevant if instance_query is False
dac_use_selfatt_ln: bool = True,
use_act_checkpoint: bool = False,
compile_mode=None,
presence_token: bool = False,
clamp_presence_logits: bool = True,
clamp_presence_logit_max_val: float = 10.0,
use_normed_output_consistently: bool = True,
separate_box_head_instance: bool = False,
separate_norm_instance: bool = False,
):
"""Initialize the TransformerDecoder."""
super().__init__()
self.d_model = d_model
self.layers = _get_clones(layer, num_layers)
self.fine_layers = (
_get_clones(interaction_layer, num_layers) if interaction_layer is not None else [None] * num_layers
)
self.num_layers = num_layers
self.num_queries = num_queries
self.dac = dac
if dac:
self.num_o2m_queries = num_queries
tot_num_queries = num_queries
else:
self.num_o2m_queries = num_o2m_queries
tot_num_queries = num_queries + num_o2m_queries
self.norm = nn.LayerNorm(d_model)
self.return_intermediate = return_intermediate
self.bbox_embed = MLP(d_model, d_model, 4, 3)
self.query_embed = nn.Embedding(tot_num_queries, d_model)
self.instance_query_embed = None
self.instance_query_reference_points = None
self.use_instance_query = instance_query
self.num_instances = num_instances
self.use_normed_output_consistently = use_normed_output_consistently
self.instance_norm = nn.LayerNorm(d_model) if separate_norm_instance else None
self.instance_bbox_embed = None
if separate_box_head_instance:
self.instance_bbox_embed = MLP(d_model, d_model, 4, 3)
if instance_query:
self.instance_query_embed = nn.Embedding(num_instances, d_model)
self.box_refine = box_refine
if box_refine:
nn.init.constant_(self.bbox_embed.layers[-1].weight.data, 0)
nn.init.constant_(self.bbox_embed.layers[-1].bias.data, 0)
self.reference_points = nn.Embedding(num_queries, 4)
if instance_query:
self.instance_reference_points = nn.Embedding(num_instances, 4)
assert boxRPB in ["none", "log", "linear", "both"]
self.boxRPB = boxRPB
if boxRPB != "none":
try:
nheads = self.layers[0].cross_attn_image.num_heads
except AttributeError:
nheads = self.layers[0].cross_attn.num_heads
n_input = 4 if boxRPB == "both" else 2
self.boxRPB_embed_x = MLP(n_input, d_model, nheads, 2)
self.boxRPB_embed_y = MLP(n_input, d_model, nheads, 2)
self.compilable_cord_cache = None
self.compilable_stored_size = None
self.coord_cache = {}
self.roi_pooler = (
RoIAlign(output_size=7, spatial_scale=1, sampling_ratio=-1, aligned=True)
if interaction_layer is not None
else None
)
if frozen:
for p in self.parameters():
p.requires_grad_(False)
self.presence_token = None
self.clamp_presence_logits = clamp_presence_logits
self.clamp_presence_logit_max_val = clamp_presence_logit_max_val
if presence_token:
self.presence_token = nn.Embedding(1, d_model)
self.presence_token_head = MLP(d_model, d_model, 1, 3)
self.presence_token_out_norm = nn.LayerNorm(d_model)
self.ref_point_head = MLP(2 * self.d_model, self.d_model, self.d_model, 2)
self.dac_use_selfatt_ln = dac_use_selfatt_ln
self.use_act_checkpoint = use_act_checkpoint
nn.init.normal_(self.query_embed.weight.data)
if self.instance_query_embed is not None:
nn.init.normal_(self.instance_query_embed.weight.data)
assert self.roi_pooler is None
assert self.return_intermediate, "support return_intermediate only"
assert self.box_refine, "support box refine only"
self.compile_mode = compile_mode
self.compiled = False
# We defer compilation till after the first forward, to first warm-up the boxRPB cache
# assign layer index to each layer so that some layers can decide what to do
# based on which layer index they are (e.g. cross attention to memory bank only
# in selected layers)
for layer_idx, layer in enumerate(self.layers):
layer.layer_idx = layer_idx
@staticmethod
def _get_coords(H, W, device, dtype):
"""Get normalized coordinates for height and width."""
coords_h = torch.arange(0, H, dtype=dtype, device=device) / H
coords_w = torch.arange(0, W, dtype=dtype, device=device) / W
return coords_h, coords_w
def _get_rpb_matrix(self, reference_boxes, feat_size):
"""Get the relative position bias (RPB) matrix for box-relative position bias."""
H, W = feat_size
boxes_xyxy = xywh2xyxy(reference_boxes).transpose(0, 1)
bs, num_queries, _ = boxes_xyxy.shape
if self.compilable_cord_cache is None:
self.compilable_cord_cache = self._get_coords(H, W, reference_boxes.device, reference_boxes.dtype)
self.compilable_stored_size = (H, W)
if torch.compiler.is_dynamo_compiling() or self.compilable_stored_size == (
H,
W,
):
# good, hitting the cache, will be compilable
coords_h, coords_w = self.compilable_cord_cache
else:
# cache miss, will create compilation issue
# In case we're not compiling, we'll still rely on the dict-based cache
if feat_size not in self.coord_cache:
self.coord_cache[feat_size] = self._get_coords(H, W, reference_boxes.device)
coords_h, coords_w = self.coord_cache[feat_size]
assert coords_h.shape == (H,)
assert coords_w.shape == (W,)
deltas_y = coords_h.view(1, -1, 1) - boxes_xyxy.reshape(-1, 1, 4)[:, :, 1:4:2]
deltas_y = deltas_y.view(bs, num_queries, -1, 2)
deltas_x = coords_w.view(1, -1, 1) - boxes_xyxy.reshape(-1, 1, 4)[:, :, 0:3:2]
deltas_x = deltas_x.view(bs, num_queries, -1, 2)
if self.boxRPB in ["log", "both"]:
deltas_x_log = deltas_x * 8 # normalize to -8, 8
deltas_x_log = torch.sign(deltas_x_log) * torch.log2(torch.abs(deltas_x_log) + 1.0) / np.log2(8)
deltas_y_log = deltas_y * 8 # normalize to -8, 8
deltas_y_log = torch.sign(deltas_y_log) * torch.log2(torch.abs(deltas_y_log) + 1.0) / np.log2(8)
if self.boxRPB == "log":
deltas_x = deltas_x_log
deltas_y = deltas_y_log
else:
deltas_x = torch.cat([deltas_x, deltas_x_log], dim=-1)
deltas_y = torch.cat([deltas_y, deltas_y_log], dim=-1)
if self.training:
assert self.use_act_checkpoint, "activation ckpt not enabled in decoder"
deltas_x = self.boxRPB_embed_x(x=deltas_x) # bs, num_queries, W, n_heads
deltas_y = self.boxRPB_embed_y(x=deltas_y) # bs, num_queries, H, n_heads
if not torch.compiler.is_dynamo_compiling():
assert deltas_x.shape[:3] == (bs, num_queries, W)
assert deltas_y.shape[:3] == (bs, num_queries, H)
B = deltas_y.unsqueeze(3) + deltas_x.unsqueeze(2) # bs, num_queries, H, W, n_heads
if not torch.compiler.is_dynamo_compiling():
assert B.shape[:4] == (bs, num_queries, H, W)
B = B.flatten(2, 3) # bs, num_queries, H*W, n_heads
B = B.permute(0, 3, 1, 2) # bs, n_heads, num_queries, H*W
B = B.contiguous() # memeff attn likes ordered strides
if not torch.compiler.is_dynamo_compiling():
assert B.shape[2:] == (num_queries, H * W)
return B
def forward(
self,
tgt,
memory,
tgt_mask: torch.Tensor = None,
memory_mask: torch.Tensor = None,
memory_key_padding_mask: torch.Tensor = None,
pos: torch.Tensor = None,
reference_boxes: torch.Tensor = None, # num_queries, bs, 4
# for memory
spatial_shapes: torch.Tensor = None, # bs, num_levels, 2
valid_ratios: torch.Tensor = None,
# for text
memory_text: torch.Tensor = None,
text_attention_mask: torch.Tensor = None,
# if `apply_dac` is None, it will default to `self.dac`
apply_dac: bool | None = None,
is_instance_prompt=False,
decoder_extra_kwargs: dict | None = None,
# ROI memory bank
obj_roi_memory_feat=None,
obj_roi_memory_mask=None,
box_head_trk=None,
):
"""Forward pass of the TransformerDecoder."""
if memory_mask is not None:
assert self.boxRPB == "none", (
"inputting a memory_mask in the presence of boxRPB is unexpected/not implemented"
)
apply_dac = apply_dac if apply_dac is not None else self.dac
if apply_dac:
assert (tgt.shape[0] == self.num_queries) or (
self.use_instance_query and (tgt.shape[0] == self.instance_query_embed.num_embeddings)
)
tgt = tgt.repeat(2, 1, 1)
# note that we don't tile tgt_mask, since DAC doesn't
# use self-attention in o2m queries
if reference_boxes is not None:
assert (reference_boxes.shape[0] == self.num_queries) or (
self.use_instance_query and (reference_boxes.shape[0] == self.instance_query_embed.num_embeddings)
)
reference_boxes = reference_boxes.repeat(2, 1, 1)
bs = tgt.shape[1]
intermediate = []
intermediate_presence_logits = []
presence_feats = None
if self.box_refine:
if reference_boxes is None:
# In this case, we're in a one-stage model, so we generate the reference boxes
reference_boxes = self.reference_points.weight.unsqueeze(1)
reference_boxes = reference_boxes.repeat(2, bs, 1) if apply_dac else reference_boxes.repeat(1, bs, 1)
reference_boxes = reference_boxes.sigmoid()
intermediate_ref_boxes = [reference_boxes]
else:
reference_boxes = None
intermediate_ref_boxes = None
output = tgt
presence_out = None
if self.presence_token is not None and is_instance_prompt is False:
# expand to batch dim
presence_out = self.presence_token.weight[None].expand(1, bs, -1)
box_head = self.bbox_embed
if is_instance_prompt and self.instance_bbox_embed is not None:
box_head = self.instance_bbox_embed
out_norm = self.norm
if is_instance_prompt and self.instance_norm is not None:
out_norm = self.instance_norm
for layer_idx, layer in enumerate(self.layers):
reference_points_input = (
reference_boxes[:, :, None] * torch.cat([valid_ratios, valid_ratios], -1)[None, :]
) # nq, bs, nlevel, 4
query_sine_embed = gen_sineembed_for_position(
reference_points_input[:, :, 0, :], self.d_model
) # nq, bs, d_model*2
# conditional query
query_pos = self.ref_point_head(query_sine_embed) # nq, bs, d_model
if self.boxRPB != "none" and reference_boxes is not None:
assert spatial_shapes.shape[0] == 1, "only single scale support implemented"
memory_mask = self._get_rpb_matrix(
reference_boxes,
(spatial_shapes[0, 0], spatial_shapes[0, 1]),
)
memory_mask = memory_mask.flatten(0, 1) # (bs*n_heads, nq, H*W)
if self.training:
assert self.use_act_checkpoint, "Activation checkpointing not enabled in the decoder"
output, presence_out = layer(
tgt=output,
tgt_query_pos=query_pos,
memory_text=memory_text,
text_attention_mask=text_attention_mask,
memory=memory,
memory_key_padding_mask=memory_key_padding_mask,
memory_pos=pos,
self_attn_mask=tgt_mask,
cross_attn_mask=memory_mask,
dac=apply_dac,
dac_use_selfatt_ln=self.dac_use_selfatt_ln,
presence_token=presence_out,
**(decoder_extra_kwargs or {}),
# ROI memory bank
obj_roi_memory_feat=obj_roi_memory_feat,
obj_roi_memory_mask=obj_roi_memory_mask,
)
# iter update
if self.box_refine:
reference_before_sigmoid = inverse_sigmoid(reference_boxes)
if box_head_trk is None:
# delta_unsig = self.bbox_embed(output)
if not self.use_normed_output_consistently:
delta_unsig = box_head(output)
else:
delta_unsig = box_head(out_norm(output))
else:
# box_head_trk use a separate box head for tracking queries
Q_det = decoder_extra_kwargs["Q_det"]
assert output.size(0) >= Q_det
delta_unsig_det = self.bbox_embed(output[:Q_det])
delta_unsig_trk = box_head_trk(output[Q_det:])
delta_unsig = torch.cat([delta_unsig_det, delta_unsig_trk], dim=0)
outputs_unsig = delta_unsig + reference_before_sigmoid
new_reference_points = outputs_unsig.sigmoid()
reference_boxes = new_reference_points.detach()
if layer_idx != self.num_layers - 1:
intermediate_ref_boxes.append(new_reference_points)
else:
raise NotImplementedError("not implemented yet")
intermediate.append(out_norm(output))
if self.presence_token is not None and is_instance_prompt is False:
# norm, mlp head
intermediate_layer_presence_logits = self.presence_token_head(
self.presence_token_out_norm(presence_out)
).squeeze(-1)
# clamp to mitigate numerical issues
if self.clamp_presence_logits:
intermediate_layer_presence_logits.clamp(
min=-self.clamp_presence_logit_max_val,
max=self.clamp_presence_logit_max_val,
)
intermediate_presence_logits.append(intermediate_layer_presence_logits)
presence_feats = presence_out.clone()
if not self.compiled and self.compile_mode is not None:
self.forward = torch.compile(self.forward, mode=self.compile_mode, fullgraph=True)
self.compiled = True
return (
torch.stack(intermediate),
torch.stack(intermediate_ref_boxes),
(
torch.stack(intermediate_presence_logits)
if self.presence_token is not None and is_instance_prompt is False
else None
),
presence_feats,
)

View File

@@ -0,0 +1,529 @@
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
# Based on https://github.com/IDEA-Research/GroundingDINO
from __future__ import annotations
import torch
from torch import nn
from ultralytics.nn.modules.utils import _get_clones
from .model_misc import get_valid_ratio
class TransformerEncoderLayer(nn.Module):
"""Transformer encoder layer that performs self-attention followed by cross-attention.
This layer was previously called TransformerDecoderLayer but was renamed to better reflect its role in the
architecture. It processes input sequences through self-attention and then cross-attention with another input
(typically image features).
The layer supports both pre-norm and post-norm configurations, as well as positional encoding at different stages of
the attention mechanism.
"""
def __init__(
self,
d_model: int,
dim_feedforward: int,
dropout: float,
pos_enc_at_attn: bool,
pos_enc_at_cross_attn_keys: bool,
pos_enc_at_cross_attn_queries: bool,
pre_norm: bool,
self_attention: nn.Module = None,
cross_attention: nn.Module = None,
):
"""Initialize a transformer encoder layer.
Args:
cross_attention: Cross-attention module for attending to image features
d_model: Model dimension/hidden size
dim_feedforward: Dimension of the feedforward network
dropout: Dropout probability
pos_enc_at_attn: Whether to add positional encodings at self-attention
pos_enc_at_cross_attn_keys: Whether to add positional encodings to keys in cross-attention
pos_enc_at_cross_attn_queries: Whether to add positional encodings to queries in cross-attention
pre_norm: Whether to use pre-norm (True) or post-norm (False) architecture
self_attention: Self-attention module
"""
super().__init__()
self.d_model = d_model
self.dim_feedforward = dim_feedforward
self.dropout_value = dropout
self.self_attn = self_attention or nn.MultiheadAttention(num_heads=8, dropout=0.1, embed_dim=256)
self.cross_attn_image = cross_attention or nn.MultiheadAttention(num_heads=8, dropout=0.1, embed_dim=256)
# Implementation of Feedforward model
self.linear1 = nn.Linear(d_model, dim_feedforward)
self.dropout = nn.Dropout(dropout)
self.linear2 = nn.Linear(dim_feedforward, d_model)
self.norm1 = nn.LayerNorm(d_model)
self.norm2 = nn.LayerNorm(d_model)
self.norm3 = nn.LayerNorm(d_model)
self.dropout1 = nn.Dropout(dropout)
self.dropout2 = nn.Dropout(dropout)
self.dropout3 = nn.Dropout(dropout)
self.activation = nn.ReLU()
self.pre_norm = pre_norm
self.pos_enc_at_attn = pos_enc_at_attn
self.pos_enc_at_cross_attn_queries = pos_enc_at_cross_attn_queries
self.pos_enc_at_cross_attn_keys = pos_enc_at_cross_attn_keys
self.layer_idx = None
def forward_post(
self,
tgt: torch.Tensor,
memory: torch.Tensor,
tgt_mask: torch.Tensor = None,
memory_mask: torch.Tensor = None,
tgt_key_padding_mask: torch.Tensor = None,
memory_key_padding_mask: torch.Tensor = None,
pos: torch.Tensor = None,
query_pos: torch.Tensor = None,
**kwargs,
) -> torch.Tensor:
"""Forward pass for post-norm architecture.
In post-norm architecture, normalization is applied after attention and feedforward operations.
Args:
tgt: Input tensor to be processed
memory: Memory tensor for cross-attention
tgt_mask: Mask for self-attention
memory_mask: Mask for cross-attention
tgt_key_padding_mask: Key padding mask for self-attention
memory_key_padding_mask: Key padding mask for cross-attention
pos: Positional encoding for memory
query_pos: Positional encoding for query
**kwargs: Additional keyword arguments
Returns:
Processed tensor
"""
q = k = tgt + query_pos if self.pos_enc_at_attn else tgt
# Self attention
tgt2 = self.self_attn(
q, k, value=tgt, attn_mask=tgt_mask, key_padding_mask=tgt_key_padding_mask, need_weights=False
)[0]
tgt = tgt + self.dropout1(tgt2)
tgt = self.norm1(tgt)
# Cross attention to image
tgt2 = self.cross_attn_image(
query=tgt + query_pos if self.pos_enc_at_cross_attn_queries else tgt,
key=memory + pos if self.pos_enc_at_cross_attn_keys else memory,
value=memory,
attn_mask=memory_mask,
key_padding_mask=memory_key_padding_mask,
need_weights=False,
)[0]
tgt = tgt + self.dropout2(tgt2)
tgt = self.norm2(tgt)
# FFN
tgt2 = self.linear2(self.dropout(self.activation(self.linear1(tgt))))
tgt = tgt + self.dropout3(tgt2)
tgt = self.norm3(tgt)
return tgt
def forward_pre(
self,
tgt: torch.Tensor,
memory: torch.Tensor,
dac: bool = False,
tgt_mask: torch.Tensor = None,
memory_mask: torch.Tensor = None,
tgt_key_padding_mask: torch.Tensor = None,
memory_key_padding_mask: torch.Tensor = None,
pos: torch.Tensor = None,
query_pos: torch.Tensor = None,
) -> torch.Tensor:
"""Forward pass for pre-norm architecture.
In pre-norm architecture, normalization is applied before attention and feedforward operations.
Args:
tgt: Input tensor to be processed
memory: Memory tensor for cross-attention
dac: Whether to use Divide-and-Conquer attention
tgt_mask: Mask for self-attention
memory_mask: Mask for cross-attention
tgt_key_padding_mask: Key padding mask for self-attention
memory_key_padding_mask: Key padding mask for cross-attention
pos: Positional encoding for memory
query_pos: Positional encoding for query
Returns:
Processed tensor
"""
if dac:
# we only apply self attention to the first half of the queries
assert tgt.shape[0] % 2 == 0
other_tgt = tgt[tgt.shape[0] // 2 :]
tgt = tgt[: tgt.shape[0] // 2]
tgt2 = self.norm1(tgt).contiguous()
q = k = tgt2 + query_pos if self.pos_enc_at_attn else tgt2
tgt2 = self.self_attn(q, k, value=tgt2, attn_mask=tgt_mask, key_padding_mask=tgt_key_padding_mask)[0]
tgt = tgt + self.dropout1(tgt2)
if dac:
# Recombine
tgt = torch.cat((tgt, other_tgt), dim=0)
tgt2 = self.norm2(tgt)
memory = memory.to(tgt2.dtype).contiguous()
tgt2 = self.cross_attn_image(
query=tgt2 + query_pos if self.pos_enc_at_cross_attn_queries else tgt2,
key=memory + pos if self.pos_enc_at_cross_attn_keys else memory,
value=memory,
attn_mask=memory_mask,
key_padding_mask=memory_key_padding_mask,
)[0]
tgt = tgt + self.dropout2(tgt2)
tgt2 = self.norm3(tgt)
tgt2 = self.linear2(self.dropout(self.activation(self.linear1(tgt2))))
tgt = tgt + self.dropout3(tgt2)
return tgt
def forward(
self,
tgt: torch.Tensor,
memory: torch.Tensor,
dac: bool = False,
tgt_mask: torch.Tensor = None,
memory_mask: torch.Tensor = None,
tgt_key_padding_mask: torch.Tensor = None,
memory_key_padding_mask: torch.Tensor = None,
pos: torch.Tensor = None,
query_pos: torch.Tensor = None,
) -> torch.Tensor:
"""Forward pass for the transformer encoder layer.
Args:
tgt: Input tensor to be processed
memory: Memory tensor (e.g., image features) for cross-attention
dac: Whether to use Divide-and-Conquer attention (only apply self-attention to first half)
tgt_mask: Mask for self-attention
memory_mask: Mask for cross-attention
tgt_key_padding_mask: Key padding mask for self-attention
memory_key_padding_mask: Key padding mask for cross-attention
pos: Positional encoding for memory
query_pos: Positional encoding for query
Returns:
Processed tensor after self-attention, cross-attention, and feedforward network
"""
fwd_fn = self.forward_pre if self.pre_norm else self.forward_post
return fwd_fn(
tgt,
memory,
dac=dac,
tgt_mask=tgt_mask,
memory_mask=memory_mask,
tgt_key_padding_mask=tgt_key_padding_mask,
memory_key_padding_mask=memory_key_padding_mask,
pos=pos,
query_pos=query_pos,
# attn_bias=attn_bias,
# **kwds,
)
class TransformerEncoder(nn.Module):
"""Transformer encoder that processes multi-level features.
This encoder takes multi-level features (e.g., from a backbone network) and processes them through a stack of
transformer encoder layers. It supports features from multiple levels (e.g., different resolutions) and can apply
activation checkpointing for memory efficiency during training.
Args:
layer: The encoder layer to be stacked multiple times
num_layers: Number of encoder layers to stack
d_model: Model dimension/hidden size
num_feature_levels: Number of feature levels to process
frozen: Whether to freeze the parameters of this module
use_act_checkpoint: Whether to use activation checkpointing during training
"""
def __init__(
self,
layer: nn.Module,
num_layers: int,
d_model: int,
num_feature_levels: int,
frozen: bool = False,
use_act_checkpoint: bool = False,
):
"""Initialize the transformer encoder."""
super().__init__()
self.layers = _get_clones(layer, num_layers)
self.num_layers = num_layers
self.num_feature_levels = num_feature_levels
self.level_embed = None
if num_feature_levels > 1:
self.level_embed = nn.Parameter(torch.Tensor(num_feature_levels, d_model))
if frozen:
for p in self.parameters():
p.requires_grad_(False)
self.use_act_checkpoint = use_act_checkpoint
# assign layer index to each layer so that some layers can decide what to do
# based on which layer index they are (e.g. cross attention to memory bank only
# in selected layers)
for layer_idx, layer in enumerate(self.layers):
layer.layer_idx = layer_idx
def _prepare_multilevel_features(self, srcs, masks, pos_embeds):
"""Prepare multi-level features for transformer encoder."""
assert len(srcs) == self.num_feature_levels, "mismatch between expected and received # of feature levels"
src_flatten = []
mask_flatten = []
lvl_pos_embed_flatten = []
spatial_shapes = []
has_mask = masks is not None and masks[0] is not None
for lvl, (src, mask, pos_embed) in enumerate(zip(srcs, masks, pos_embeds)):
_, _, h, w = src.shape
spatial_shape = (h, w)
spatial_shapes.append(spatial_shape)
src = src.flatten(2).transpose(1, 2) # bs, hw, c
if has_mask:
mask = mask.flatten(1)
pos_embed = pos_embed.flatten(2).transpose(1, 2) # bs, hw, c
if self.level_embed is not None:
lvl_pos_embed = pos_embed + self.level_embed[lvl].view(1, 1, -1)
else:
lvl_pos_embed = pos_embed
lvl_pos_embed_flatten.append(lvl_pos_embed)
src_flatten.append(src)
if has_mask:
mask_flatten.append(mask)
src_flatten = torch.cat(src_flatten, 1) # bs, \sum{hxw}, c
mask_flatten = torch.cat(mask_flatten, 1) if has_mask else None # bs, \sum{hxw}
lvl_pos_embed_flatten = torch.cat(lvl_pos_embed_flatten, 1) # bs, \sum{hxw}, c
spatial_shapes = torch.tensor(spatial_shapes, dtype=torch.long, device=src_flatten.device)
level_start_index = torch.cat(
(
spatial_shapes.new_zeros((1,)),
spatial_shapes.prod(1).cumsum(0)[:-1],
)
)
if has_mask:
valid_ratios = torch.stack([get_valid_ratio(m) for m in masks], 1)
else:
valid_ratios = torch.ones(
(src_flatten.shape[0], self.num_feature_levels, 2),
device=src_flatten.device,
dtype=src_flatten.dtype,
)
return (
src_flatten,
mask_flatten,
lvl_pos_embed_flatten,
level_start_index,
valid_ratios,
spatial_shapes,
)
def forward(
self,
src: list[torch.Tensor],
src_key_padding_masks: list[torch.Tensor] | None = None,
pos: list[torch.Tensor] | None = None,
prompt: torch.Tensor = None,
prompt_key_padding_mask: torch.Tensor = None,
encoder_extra_kwargs: dict | None = None,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
"""Process multi-level features through the transformer encoder.
Args:
src: List of multi-level features, each with shape (batch_size, channels, height, width)
src_key_padding_masks: List of padding masks for each feature level, each with shape (batch_size, height,
width)
pos: List of positional embeddings for each feature level, each with shape (batch_size, channels, height,
width)
prompt: Optional text/prompt features to attend to, with shape (seq_len, batch_size, d_model)
prompt_key_padding_mask: Optional padding mask for prompt, with shape (batch_size, seq_len)
encoder_extra_kwargs: Optional additional arguments to pass to each encoder layer
Returns:
A tuple containing:
- output: Processed features with shape (seq_len, batch_size, d_model)
- key_padding_masks_flatten: Flattened padding masks
- lvl_pos_embed_flatten: Flattened positional embeddings
- level_start_index: Starting indices for each feature level
- spatial_shapes: Spatial dimensions of each feature level
- valid_ratios: Valid ratios for each feature level
"""
assert len(src) == self.num_feature_levels, "must be equal to num_feature_levels"
if src_key_padding_masks is not None:
assert len(src_key_padding_masks) == self.num_feature_levels
if pos is not None:
assert len(pos) == self.num_feature_levels
# Flatten multilevel feats and add level pos embeds
(
src_flatten,
key_padding_masks_flatten,
lvl_pos_embed_flatten,
level_start_index,
valid_ratios,
spatial_shapes,
) = self._prepare_multilevel_features(src, src_key_padding_masks, pos)
output = src_flatten
for layer in self.layers:
layer_kwargs = {}
assert isinstance(layer, TransformerEncoderLayer)
layer_kwargs["memory"] = prompt
layer_kwargs["memory_key_padding_mask"] = prompt_key_padding_mask
layer_kwargs["query_pos"] = lvl_pos_embed_flatten
layer_kwargs["tgt"] = output
layer_kwargs["tgt_key_padding_mask"] = key_padding_masks_flatten
if self.training:
assert self.use_act_checkpoint, "activation ckpt not enabled in encoder"
if encoder_extra_kwargs is not None:
layer_kwargs.update(encoder_extra_kwargs)
output = layer(**layer_kwargs)
# return as seq first
return (
output.transpose(0, 1),
(key_padding_masks_flatten.transpose(0, 1) if key_padding_masks_flatten is not None else None),
lvl_pos_embed_flatten.transpose(0, 1),
level_start_index,
spatial_shapes,
valid_ratios,
)
class TransformerEncoderFusion(TransformerEncoder):
"""Transformer encoder that fuses text and image features.
This encoder extends TransformerEncoder to handle both text and image features, with the ability to add pooled text
features to image features for better cross-modal fusion. It supports torch.compile for performance optimization.
Args:
layer: The encoder layer to be stacked multiple times
num_layers: Number of encoder layers to stack
d_model: Model dimension/hidden size
num_feature_levels: Number of feature levels to process
add_pooled_text_to_img_feat: Whether to add pooled text features to image features
pool_text_with_mask: Whether to use the mask when pooling text features
compile_mode: Mode for torch.compile, or None to disable compilation
**kwargs: Additional arguments to pass to the parent class
"""
def __init__(
self,
layer: nn.Module,
num_layers: int,
d_model: int,
num_feature_levels: int,
add_pooled_text_to_img_feat: bool = True,
pool_text_with_mask: bool = False,
compile_mode: str | None = None,
**kwargs,
):
"""Initialize the transformer encoder with text-image fusion."""
super().__init__(
layer,
num_layers,
d_model,
num_feature_levels,
**kwargs,
)
self.add_pooled_text_to_img_feat = add_pooled_text_to_img_feat
if self.add_pooled_text_to_img_feat:
self.text_pooling_proj = nn.Linear(d_model, d_model)
self.pool_text_with_mask = pool_text_with_mask
if compile_mode is not None:
self.forward = torch.compile(self.forward, mode=compile_mode, fullgraph=True)
def forward(
self,
src: list[torch.Tensor],
prompt: torch.Tensor,
src_key_padding_mask: list[torch.Tensor] | None = None,
src_pos: list[torch.Tensor] | None = None,
prompt_key_padding_mask: torch.Tensor = None,
feat_sizes: list[int] | None = None,
encoder_extra_kwargs: dict | None = None,
):
"""Forward pass for the transformer encoder with text-image fusion."""
# Restore spatial shapes of vision
bs = src[0].shape[1] # seq first
if feat_sizes is not None:
assert len(feat_sizes) == len(src)
if src_key_padding_mask is None:
src_key_padding_mask = [None] * len(src)
for i, (h, w) in enumerate(feat_sizes):
src[i] = src[i].reshape(h, w, bs, -1).permute(2, 3, 0, 1)
src_pos[i] = src_pos[i].reshape(h, w, bs, -1).permute(2, 3, 0, 1)
src_key_padding_mask[i] = (
src_key_padding_mask[i].reshape(h, w, bs).permute(2, 0, 1)
if src_key_padding_mask[i] is not None
else None
)
else:
assert all(x.dim == 4 for x in src), "expected list of (bs, c, h, w) tensors"
if self.add_pooled_text_to_img_feat:
# Fusion: Add mean pooled text to image features
pooled_text = pool_text_feat(prompt, prompt_key_padding_mask, self.pool_text_with_mask)
pooled_text = self.text_pooling_proj(pooled_text)[..., None, None] # prompt is seq first
src = [x.add_(pooled_text) for x in src]
(
out,
key_padding_masks_flatten,
lvl_pos_embed_flatten,
level_start_index,
spatial_shapes,
valid_ratios,
) = super().forward(
src,
src_key_padding_masks=src_key_padding_mask,
pos=src_pos,
prompt=prompt.transpose(0, 1),
prompt_key_padding_mask=prompt_key_padding_mask,
encoder_extra_kwargs=encoder_extra_kwargs,
)
return {
"memory": out,
"padding_mask": key_padding_masks_flatten,
"pos_embed": lvl_pos_embed_flatten,
"memory_text": prompt,
"level_start_index": level_start_index,
"spatial_shapes": spatial_shapes,
"valid_ratios": valid_ratios,
}
def pool_text_feat(prompt, prompt_mask, pool_with_mask):
"""Mean-pool the prompt embeddings over the valid tokens only."""
# prompt has shape (seq, bs, dim)
if not pool_with_mask:
return prompt.mean(dim=0)
# prompt_mask has shape (bs, seq), where False is valid and True is padding
assert prompt_mask.dim() == 2
# is_valid has shape (seq, bs, 1), where 1 is valid and 0 is padding
is_valid = (~prompt_mask).float().permute(1, 0)[..., None]
# num_valid has shape (bs, 1)
num_valid = torch.clamp(torch.sum(is_valid, dim=0), min=1.0)
# mean pool over all the valid tokens
pooled_text = (prompt * is_valid).sum(dim=0) / num_valid
return pooled_text

View File

@@ -0,0 +1,415 @@
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
import torch
import torch.nn as nn
import torchvision
from ultralytics.nn.modules.utils import _get_clones
from ultralytics.utils.ops import xywh2xyxy
def is_right_padded(mask: torch.Tensor):
"""Given a padding mask (following pytorch convention, 1s for padded values), returns whether the padding is on the
right or not.
"""
return (mask.long() == torch.sort(mask.long(), dim=-1)[0]).all()
def concat_padded_sequences(seq1, mask1, seq2, mask2, return_index: bool = False):
"""
Concatenates two right-padded sequences, such that the resulting sequence
is contiguous and also right-padded.
Following pytorch's convention, tensors are sequence first, and the mask are
batch first, with 1s for padded values.
:param seq1: A tensor of shape (seq1_length, batch_size, hidden_size).
:param mask1: A tensor of shape (batch_size, seq1_length).
:param seq2: A tensor of shape (seq2_length, batch_size, hidden_size).
:param mask2: A tensor of shape (batch_size, seq2_length).
:param return_index: If True, also returns the index of the ids of the element of seq2
in the concatenated sequence. This can be used to retrieve the elements of seq2
:return: A tuple (concatenated_sequence, concatenated_mask) if return_index is False,
otherwise (concatenated_sequence, concatenated_mask, index).
"""
seq1_length, batch_size, hidden_size = seq1.shape
seq2_length, batch_size, hidden_size = seq2.shape
assert batch_size == seq1.size(1) == seq2.size(1) == mask1.size(0) == mask2.size(0)
assert hidden_size == seq1.size(2) == seq2.size(2)
assert seq1_length == mask1.size(1)
assert seq2_length == mask2.size(1)
torch._assert(is_right_padded(mask1), "Mask is not right padded")
torch._assert(is_right_padded(mask2), "Mask is not right padded")
actual_seq1_lengths = (~mask1).sum(dim=-1)
actual_seq2_lengths = (~mask2).sum(dim=-1)
final_lengths = actual_seq1_lengths + actual_seq2_lengths
max_length = seq1_length + seq2_length
concatenated_mask = (
torch.arange(max_length, device=seq2.device)[None].repeat(batch_size, 1) >= final_lengths[:, None]
)
# (max_len, batch_size, hidden_size)
concatenated_sequence = torch.zeros((max_length, batch_size, hidden_size), device=seq2.device, dtype=seq2.dtype)
concatenated_sequence[:seq1_length, :, :] = seq1
# At this point, the element of seq1 are in the right place
# We just need to shift the elements of seq2
index = torch.arange(seq2_length, device=seq2.device)[:, None].repeat(1, batch_size)
index = index + actual_seq1_lengths[None]
concatenated_sequence = concatenated_sequence.scatter(0, index[:, :, None].expand(-1, -1, hidden_size), seq2)
if return_index:
return concatenated_sequence, concatenated_mask, index
return concatenated_sequence, concatenated_mask
class Prompt:
"""Utility class to manipulate geometric prompts.
We expect the sequences in pytorch convention, that is sequence first, batch second The dimensions are expected as
follows: box_embeddings shape: N_boxes x B x C_box box_mask shape: B x N_boxes. Can be None if nothing is masked out
point_embeddings shape: N_points x B x C_point point_mask shape: B x N_points. Can be None if nothing is masked out
mask_embeddings shape: N_masks x B x 1 x H_mask x W_mask mask_mask shape: B x N_masks. Can be None if nothing is
masked out
We also store positive/negative labels. These tensors are also stored batch-first If they are None, we'll assume
positive labels everywhere box_labels: long tensor of shape N_boxes x B point_labels: long tensor of shape N_points
x B mask_labels: long tensor of shape N_masks x B
"""
def __init__(self, box_embeddings=None, box_mask=None, box_labels=None):
"""Initialize the Prompt object."""
# Check for null prompt
# Check for null prompt
if box_embeddings is None:
self.box_embeddings = None
self.box_labels = None
self.box_mask = None
return
# Get sequence length, batch size, and device
box_seq_len = box_embeddings.shape[0]
bs = box_embeddings.shape[1]
device = box_embeddings.device
# Initialize labels and attention mask if not provided
if box_labels is None:
box_labels = torch.ones(box_seq_len, bs, device=device, dtype=torch.long)
if box_mask is None:
box_mask = torch.zeros(bs, box_seq_len, device=device, dtype=torch.bool)
# Dimension checks
assert list(box_embeddings.shape[:2]) == [box_seq_len, bs], (
f"Wrong dimension for box embeddings. Expected [{box_seq_len}, {bs}, *] got {box_embeddings.shape}"
)
assert box_embeddings.shape[-1] == 4, (
f"Expected box embeddings to have 4 coordinates, got {box_embeddings.shape[-1]}"
)
assert list(box_mask.shape) == [bs, box_seq_len], (
f"Wrong dimension for box mask. Expected [{bs}, {box_seq_len}] got {box_mask.shape}"
)
assert list(box_labels.shape) == [box_seq_len, bs], (
f"Wrong dimension for box labels. Expected [{box_seq_len}, {bs}] got {box_labels.shape}"
)
# Device checks
assert box_embeddings.device == device, (
f"Expected box embeddings to be on device {device}, got {box_embeddings.device}"
)
assert box_mask.device == device, f"Expected box mask to be on device {device}, got {box_mask.device}"
assert box_labels.device == device, f"Expected box labels to be on device {device}, got {box_labels.device}"
self.box_embeddings = box_embeddings
self.box_mask = box_mask
self.box_labels = box_labels
def append_boxes(self, boxes, labels=None, mask=None):
"""Append box prompts to existing prompts.
Args:
boxes: Tensor of shape (N_new_boxes, B, 4) with normalized box coordinates
labels: Optional tensor of shape (N_new_boxes, B) with positive/negative labels
mask: Optional tensor of shape (B, N_new_boxes) for attention mask
"""
if self.box_embeddings is None:
# First boxes - initialize
self.box_embeddings = boxes
bs = boxes.shape[1]
box_seq_len = boxes.shape[0]
if labels is None:
labels = torch.ones(box_seq_len, bs, device=boxes.device, dtype=torch.long)
if mask is None:
mask = torch.zeros(bs, box_seq_len, device=boxes.device, dtype=torch.bool)
self.box_labels = labels
self.box_mask = mask
return
# Append to existing boxes
bs = self.box_embeddings.shape[1]
assert boxes.shape[1] == bs, f"Batch size mismatch: expected {bs}, got {boxes.shape[1]}"
if labels is None:
labels = torch.ones(boxes.shape[0], bs, device=boxes.device, dtype=torch.long)
if mask is None:
mask = torch.zeros(bs, boxes.shape[0], dtype=torch.bool, device=boxes.device)
assert list(boxes.shape[:2]) == list(labels.shape[:2]), (
f"Shape mismatch between boxes {boxes.shape} and labels {labels.shape}"
)
# Concatenate using the helper function
self.box_labels, _ = concat_padded_sequences(
self.box_labels.unsqueeze(-1), self.box_mask, labels.unsqueeze(-1), mask
)
self.box_labels = self.box_labels.squeeze(-1)
self.box_embeddings, self.box_mask = concat_padded_sequences(self.box_embeddings, self.box_mask, boxes, mask)
class SequenceGeometryEncoder(nn.Module):
"""Encoder for geometric box prompts. Assumes boxes are passed in the "normalized CxCyWH" format.
Boxes can be encoded with any of the three possibilities:
- direct projection: linear projection from coordinate space to d_model
- pooling: RoI align features from the backbone
- pos encoder: position encoding of the box center
These three options are mutually compatible and will be summed if multiple are selected.
As an alternative, boxes can be encoded as two corner points (top-left and bottom-right).
The encoded sequence can be further processed with a transformer.
"""
def __init__(
self,
encode_boxes_as_points: bool,
boxes_direct_project: bool,
boxes_pool: bool,
boxes_pos_enc: bool,
d_model: int,
pos_enc,
num_layers: int,
layer: nn.Module,
roi_size: int = 7,
add_cls: bool = True,
add_post_encode_proj: bool = True,
use_act_ckpt: bool = False,
):
"""Initialize the SequenceGeometryEncoder."""
super().__init__()
self.d_model = d_model
self.pos_enc = pos_enc
self.encode_boxes_as_points = encode_boxes_as_points
self.roi_size = roi_size
# Label embeddings: 2 labels if encoding as boxes (pos/neg)
# 6 labels if encoding as points (regular pos/neg, top-left pos/neg, bottom-right pos/neg)
num_labels = 6 if self.encode_boxes_as_points else 2
self.label_embed = torch.nn.Embedding(num_labels, self.d_model)
# CLS token for pooling
self.cls_embed = None
if add_cls:
self.cls_embed = torch.nn.Embedding(1, self.d_model)
# Point encoding (used when encode_boxes_as_points is True)
if encode_boxes_as_points:
self.points_direct_project = nn.Linear(2, self.d_model)
self.points_pool_project = None
self.points_pos_enc_project = None
else:
# Box encoding modules
assert boxes_direct_project or boxes_pos_enc or boxes_pool, "Error: need at least one way to encode boxes"
self.points_direct_project = None
self.points_pool_project = None
self.points_pos_enc_project = None
self.boxes_direct_project = None
self.boxes_pool_project = None
self.boxes_pos_enc_project = None
if boxes_direct_project:
self.boxes_direct_project = nn.Linear(4, self.d_model)
if boxes_pool:
self.boxes_pool_project = nn.Conv2d(self.d_model, self.d_model, self.roi_size)
if boxes_pos_enc:
self.boxes_pos_enc_project = nn.Linear(self.d_model + 2, self.d_model)
self.final_proj = None
if add_post_encode_proj:
self.final_proj = nn.Linear(self.d_model, self.d_model)
self.norm = nn.LayerNorm(self.d_model)
self.img_pre_norm = nn.Identity()
if self.points_pool_project is not None or self.boxes_pool_project is not None:
self.img_pre_norm = nn.LayerNorm(self.d_model)
self.encode = None
if num_layers > 0:
assert add_cls, "It's currently highly recommended to add a CLS when using a transformer"
self.encode = _get_clones(layer, num_layers)
self.encode_norm = nn.LayerNorm(self.d_model)
self.use_act_ckpt = use_act_ckpt
def _encode_points(self, points, points_mask, points_labels, img_feats):
"""Encode points (used when boxes are converted to corner points)."""
# Direct projection of coordinates
points_embed = self.points_direct_project(points.to(img_feats.dtype))
# Add label embeddings
type_embed = self.label_embed(points_labels.long())
return type_embed + points_embed, points_mask
def _encode_boxes(self, boxes, boxes_mask, boxes_labels, img_feats: torch.Tensor):
"""Encode boxes using configured encoding methods."""
boxes_embed = None
n_boxes, bs = boxes.shape[:2]
if self.boxes_direct_project is not None:
proj = self.boxes_direct_project(boxes.to(img_feats.dtype))
boxes_embed = proj
if self.boxes_pool_project is not None:
H, W = img_feats.shape[-2:]
# Convert boxes to xyxy format and denormalize
boxes_xyxy = xywh2xyxy(boxes.to(img_feats.dtype))
scale = torch.tensor([W, H, W, H], dtype=boxes_xyxy.dtype)
scale = scale.to(device=boxes_xyxy.device, non_blocking=True)
scale = scale.view(1, 1, 4)
boxes_xyxy = boxes_xyxy * scale
# RoI align
sampled = torchvision.ops.roi_align(img_feats, boxes_xyxy.transpose(0, 1).unbind(0), self.roi_size)
assert list(sampled.shape) == [
bs * n_boxes,
self.d_model,
self.roi_size,
self.roi_size,
]
proj = self.boxes_pool_project(sampled)
proj = proj.view(bs, n_boxes, self.d_model).transpose(0, 1)
if boxes_embed is None:
boxes_embed = proj
else:
boxes_embed = boxes_embed + proj
if self.boxes_pos_enc_project is not None:
cx, cy, w, h = boxes.unbind(-1)
enc = self.pos_enc.encode_boxes(cx.flatten(), cy.flatten(), w.flatten(), h.flatten())
enc = enc.view(boxes.shape[0], boxes.shape[1], enc.shape[-1])
proj = self.boxes_pos_enc_project(enc.to(img_feats.dtype))
if boxes_embed is None:
boxes_embed = proj
else:
boxes_embed = boxes_embed + proj
# Add label embeddings
type_embed = self.label_embed(boxes_labels.long())
return type_embed + boxes_embed, boxes_mask
def forward(self, geo_prompt: Prompt, img_feats, img_sizes, img_pos_embeds=None):
"""Encode geometric box prompts.
Args:
geo_prompt: Prompt object containing box embeddings, masks, and labels
img_feats: List of image features from backbone
img_sizes: List of (H, W) tuples for each feature level
img_pos_embeds: Optional position embeddings for image features
Returns:
Tuple of (encoded_embeddings, attention_mask)
"""
boxes = geo_prompt.box_embeddings
boxes_mask = geo_prompt.box_mask
boxes_labels = geo_prompt.box_labels
seq_first_img_feats = img_feats[-1] # [H*W, B, C]
seq_first_img_pos_embeds = (
img_pos_embeds[-1] if img_pos_embeds is not None else torch.zeros_like(seq_first_img_feats)
)
# Prepare image features for pooling if needed
if self.points_pool_project or self.boxes_pool_project:
assert len(img_feats) == len(img_sizes)
cur_img_feat = img_feats[-1]
cur_img_feat = self.img_pre_norm(cur_img_feat)
H, W = img_sizes[-1]
assert cur_img_feat.shape[0] == H * W
N, C = cur_img_feat.shape[-2:]
# Reshape to NxCxHxW
cur_img_feat = cur_img_feat.permute(1, 2, 0)
cur_img_feat = cur_img_feat.view(N, C, H, W)
img_feats = cur_img_feat
if self.encode_boxes_as_points:
# Convert boxes to corner points
assert boxes is not None and boxes.shape[-1] == 4
boxes_xyxy = xywh2xyxy(boxes)
top_left, bottom_right = boxes_xyxy.split(split_size=2, dim=-1)
# Adjust labels for corner points (offset by 2 and 4)
labels_tl = boxes_labels + 2
labels_br = boxes_labels + 4
# Concatenate top-left and bottom-right points
points = torch.cat([top_left, bottom_right], dim=0)
points_labels = torch.cat([labels_tl, labels_br], dim=0)
points_mask = torch.cat([boxes_mask, boxes_mask], dim=1)
final_embeds, final_mask = self._encode_points(
points=points,
points_mask=points_mask,
points_labels=points_labels,
img_feats=img_feats,
)
else:
# Encode boxes directly
final_embeds, final_mask = self._encode_boxes(
boxes=boxes,
boxes_mask=boxes_mask,
boxes_labels=boxes_labels,
img_feats=img_feats,
)
bs = final_embeds.shape[1]
assert final_mask.shape[0] == bs
# Add CLS token if configured
if self.cls_embed is not None:
cls = self.cls_embed.weight.view(1, 1, self.d_model).repeat(1, bs, 1)
cls_mask = torch.zeros(bs, 1, dtype=final_mask.dtype, device=final_mask.device)
final_embeds, final_mask = concat_padded_sequences(final_embeds, final_mask, cls, cls_mask)
# Final projection
if self.final_proj is not None:
final_embeds = self.norm(self.final_proj(final_embeds))
# Transformer encoding layers
if self.encode is not None:
for lay in self.encode:
final_embeds = lay(
tgt=final_embeds,
memory=seq_first_img_feats,
tgt_key_padding_mask=final_mask,
pos=seq_first_img_pos_embeds,
)
final_embeds = self.encode_norm(final_embeds)
return final_embeds, final_mask

View File

@@ -0,0 +1,286 @@
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
from __future__ import annotations
import math
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.utils.checkpoint as checkpoint
from ultralytics.nn.modules.transformer import MLP
class LinearPresenceHead(nn.Sequential):
"""Linear presence head for predicting the presence of classes in an image."""
def __init__(self, d_model):
"""Initializes the LinearPresenceHead."""
# a hack to make `LinearPresenceHead` compatible with old checkpoints
super().__init__(nn.Identity(), nn.Identity(), nn.Linear(d_model, 1))
def forward(self, hs, prompt, prompt_mask):
"""Forward pass of the presence head."""
return super().forward(hs)
class MaskPredictor(nn.Module):
"""Predicts masks from object queries and pixel embeddings."""
def __init__(self, hidden_dim, mask_dim):
"""Initializes the MaskPredictor."""
super().__init__()
self.mask_embed = MLP(hidden_dim, hidden_dim, mask_dim, 3)
def forward(self, obj_queries, pixel_embed):
"""Predicts masks from object queries and pixel embeddings."""
if len(obj_queries.shape) == 3:
if pixel_embed.ndim == 3:
# batch size was omitted
mask_preds = torch.einsum("bqc,chw->bqhw", self.mask_embed(obj_queries), pixel_embed)
else:
mask_preds = torch.einsum("bqc,bchw->bqhw", self.mask_embed(obj_queries), pixel_embed)
else:
# Assumed to have aux masks
if pixel_embed.ndim == 3:
# batch size was omitted
mask_preds = torch.einsum("lbqc,chw->lbqhw", self.mask_embed(obj_queries), pixel_embed)
else:
mask_preds = torch.einsum("lbqc,bchw->lbqhw", self.mask_embed(obj_queries), pixel_embed)
return mask_preds
class SegmentationHead(nn.Module):
"""Segmentation head that predicts masks from backbone features and object queries."""
def __init__(
self,
hidden_dim,
upsampling_stages,
use_encoder_inputs=False,
aux_masks=False,
no_dec=False,
pixel_decoder=None,
act_ckpt=False,
shared_conv=False,
compile_mode_pixel_decoder=None,
):
"""Initializes the SegmentationHead."""
super().__init__()
self.use_encoder_inputs = use_encoder_inputs
self.aux_masks = aux_masks
if pixel_decoder is not None:
self.pixel_decoder = pixel_decoder
else:
self.pixel_decoder = PixelDecoder(
hidden_dim,
upsampling_stages,
shared_conv=shared_conv,
compile_mode=compile_mode_pixel_decoder,
)
self.no_dec = no_dec
if no_dec:
self.mask_predictor = nn.Conv2d(hidden_dim, 1, kernel_size=3, stride=1, padding=1)
else:
self.mask_predictor = MaskPredictor(hidden_dim, mask_dim=hidden_dim)
self.act_ckpt = act_ckpt
# used to update the output dictionary
self.instance_keys = ["pred_masks"]
def _embed_pixels(self, backbone_feats: list[torch.Tensor], encoder_hidden_states) -> torch.Tensor:
"""Embeds pixels using the pixel decoder."""
if self.use_encoder_inputs:
backbone_visual_feats = [bb_feat.clone() for bb_feat in backbone_feats]
# Extract visual embeddings
encoder_hidden_states = encoder_hidden_states.permute(1, 2, 0)
spatial_dim = math.prod(backbone_feats[-1].shape[-2:])
encoder_visual_embed = encoder_hidden_states[..., :spatial_dim].reshape(-1, *backbone_feats[-1].shape[1:])
backbone_visual_feats[-1] = encoder_visual_embed
if self.act_ckpt:
pixel_embed = checkpoint.checkpoint(self.pixel_decoder, backbone_visual_feats, use_reentrant=False)
else:
pixel_embed = self.pixel_decoder(backbone_visual_feats)
else:
backbone_feats = [x for x in backbone_feats]
pixel_embed = self.pixel_decoder(backbone_feats)
if pixel_embed.shape[0] == 1:
# For batch_size=1 training, we can avoid the indexing to save memory
pixel_embed = pixel_embed.squeeze(0)
else:
pixel_embed = pixel_embed[[0], ...]
return pixel_embed
def forward(
self,
backbone_feats: list[torch.Tensor],
obj_queries: torch.Tensor,
encoder_hidden_states: torch.Tensor = None,
**kwargs,
) -> dict[str, torch.Tensor]:
"""Forward pass of the SegmentationHead."""
if self.use_encoder_inputs:
assert encoder_hidden_states is not None
pixel_embed = self._embed_pixels(backbone_feats=backbone_feats, encoder_hidden_states=encoder_hidden_states)
if self.no_dec:
mask_pred = self.mask_predictor(pixel_embed)
elif self.aux_masks:
mask_pred = self.mask_predictor(obj_queries, pixel_embed)
else:
mask_pred = self.mask_predictor(obj_queries[-1], pixel_embed)
return {"pred_masks": mask_pred}
class PixelDecoder(nn.Module):
"""Pixel decoder module that upsamples backbone features."""
def __init__(
self,
hidden_dim,
num_upsampling_stages,
interpolation_mode="nearest",
shared_conv=False,
compile_mode=None,
):
"""Initializes the PixelDecoder."""
super().__init__()
self.hidden_dim = hidden_dim
self.num_upsampling_stages = num_upsampling_stages
self.interpolation_mode = interpolation_mode
conv_layers = []
norms = []
num_convs = 1 if shared_conv else num_upsampling_stages
for _ in range(num_convs):
conv_layers.append(nn.Conv2d(self.hidden_dim, self.hidden_dim, 3, 1, 1))
norms.append(nn.GroupNorm(8, self.hidden_dim))
self.conv_layers = nn.ModuleList(conv_layers)
self.norms = nn.ModuleList(norms)
self.shared_conv = shared_conv
self.out_dim = self.conv_layers[-1].out_channels
if compile_mode is not None:
self.forward = torch.compile(self.forward, mode=compile_mode, dynamic=True, fullgraph=True)
# Needed to make checkpointing happy. But we don't know if the module is checkpointed, so we disable it by default.
torch._dynamo.config.optimize_ddp = False
def forward(self, backbone_feats: list[torch.Tensor]):
"""Forward pass of the PixelDecoder."""
prev_fpn = backbone_feats[-1]
fpn_feats = backbone_feats[:-1]
for layer_idx, bb_feat in enumerate(fpn_feats[::-1]):
curr_fpn = bb_feat
prev_fpn = curr_fpn + F.interpolate(prev_fpn, size=curr_fpn.shape[-2:], mode=self.interpolation_mode)
if self.shared_conv:
# only one conv layer
layer_idx = 0
prev_fpn = self.conv_layers[layer_idx](prev_fpn)
prev_fpn = F.relu(self.norms[layer_idx](prev_fpn))
return prev_fpn
class UniversalSegmentationHead(SegmentationHead):
"""This module handles semantic+instance segmentation."""
def __init__(
self,
hidden_dim,
upsampling_stages,
pixel_decoder,
aux_masks=False,
no_dec=False,
act_ckpt=False,
presence_head: bool = False,
dot_product_scorer=None,
cross_attend_prompt=None,
):
"""Initializes the UniversalSegmentationHead."""
super().__init__(
hidden_dim=hidden_dim,
upsampling_stages=upsampling_stages,
use_encoder_inputs=True,
aux_masks=aux_masks,
no_dec=no_dec,
pixel_decoder=pixel_decoder,
act_ckpt=act_ckpt,
)
self.d_model = hidden_dim
if dot_product_scorer is not None:
assert presence_head, "Specifying a dot product scorer without a presence head is likely a mistake"
self.presence_head = None
if presence_head:
self.presence_head = (
dot_product_scorer if dot_product_scorer is not None else LinearPresenceHead(self.d_model)
)
self.cross_attend_prompt = cross_attend_prompt
if self.cross_attend_prompt is not None:
self.cross_attn_norm = nn.LayerNorm(self.d_model)
self.semantic_seg_head = nn.Conv2d(self.pixel_decoder.out_dim, 1, kernel_size=1)
self.instance_seg_head = nn.Conv2d(self.pixel_decoder.out_dim, self.d_model, kernel_size=1)
def forward(
self,
backbone_feats: list[torch.Tensor],
obj_queries: torch.Tensor,
encoder_hidden_states: torch.Tensor = None,
prompt: torch.Tensor = None,
prompt_mask: torch.Tensor = None,
**kwargs,
) -> dict[str, torch.Tensor]:
"""Forward pass of the UniversalSegmentationHead."""
assert encoder_hidden_states is not None
bs = encoder_hidden_states.shape[1]
if self.cross_attend_prompt is not None:
tgt2 = self.cross_attn_norm(encoder_hidden_states)
tgt2 = self.cross_attend_prompt(
query=tgt2,
key=prompt.to(tgt2.dtype),
value=prompt.to(tgt2.dtype),
key_padding_mask=prompt_mask,
need_weights=False,
)[0]
encoder_hidden_states = tgt2 + encoder_hidden_states
presence_logit = None
if self.presence_head is not None:
pooled_enc = encoder_hidden_states.mean(0)
presence_logit = (
self.presence_head(
pooled_enc.view(1, bs, 1, self.d_model),
prompt=prompt,
prompt_mask=prompt_mask,
)
.squeeze(0)
.squeeze(1)
)
pixel_embed = self._embed_pixels(backbone_feats=backbone_feats, encoder_hidden_states=encoder_hidden_states)
instance_embeds = self.instance_seg_head(pixel_embed)
if self.no_dec:
mask_pred = self.mask_predictor(instance_embeds)
elif self.aux_masks:
mask_pred = self.mask_predictor(obj_queries, instance_embeds)
else:
mask_pred = self.mask_predictor(obj_queries[-1], instance_embeds)
return {
"pred_masks": mask_pred,
"semantic_seg": self.semantic_seg_head(pixel_embed),
"presence_logit": presence_logit,
}

View File

@@ -0,0 +1,199 @@
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
"""Various utility models."""
from __future__ import annotations
import math
import numpy as np
import torch
from torch import Tensor, nn
class DotProductScoring(torch.nn.Module):
"""A module that computes dot-product scores between a set of query features and a."""
def __init__(
self,
d_model,
d_proj,
prompt_mlp=None,
clamp_logits=True,
clamp_max_val=12.0,
):
"""Initialize the DotProductScoring module."""
super().__init__()
self.d_proj = d_proj
assert isinstance(prompt_mlp, torch.nn.Module) or prompt_mlp is None
self.prompt_mlp = prompt_mlp # an optional MLP projection for prompt
self.prompt_proj = torch.nn.Linear(d_model, d_proj)
self.hs_proj = torch.nn.Linear(d_model, d_proj)
self.scale = float(1.0 / np.sqrt(d_proj))
self.clamp_logits = clamp_logits
if self.clamp_logits:
self.clamp_max_val = clamp_max_val
@staticmethod
def mean_pool_text(prompt, prompt_mask):
"""Mean-pool the prompt embeddings over the valid tokens only."""
# is_valid has shape (seq, bs, 1), where 1 is valid and 0 is padding
is_valid = (~prompt_mask).to(prompt.dtype).permute(1, 0)[..., None]
# num_valid has shape (bs, 1)
num_valid = torch.clamp(torch.sum(is_valid, dim=0), min=1.0)
# mean pool over all the valid tokens -- pooled_prompt has shape (bs, proj_dim)
pooled_prompt = (prompt * is_valid).sum(dim=0) / num_valid
return pooled_prompt
def forward(self, hs, prompt, prompt_mask):
"""Compute dot-product scores between hs and prompt."""
# hs has shape (num_layer, bs, num_query, d_model)
# prompt has shape (seq, bs, d_model)
# prompt_mask has shape (bs, seq), where 1 is valid and 0 is padding
assert hs.dim() == 4 and prompt.dim() == 3 and prompt_mask.dim() == 2
# apply MLP on prompt if specified
if self.prompt_mlp is not None:
prompt = self.prompt_mlp(prompt.to(hs.dtype))
# first, get the mean-pooled version of the prompt
pooled_prompt = self.mean_pool_text(prompt, prompt_mask)
# then, project pooled_prompt and hs to d_proj dimensions
proj_pooled_prompt = self.prompt_proj(pooled_prompt) # (bs, d_proj)
proj_hs = self.hs_proj(hs) # (num_layer, bs, num_query, d_proj)
# finally, get dot-product scores of shape (num_layer, bs, num_query, 1)
scores = torch.matmul(proj_hs, proj_pooled_prompt.unsqueeze(-1))
scores *= self.scale
# clamp scores to a max value to avoid numerical issues in loss or matcher
if self.clamp_logits:
scores.clamp_(min=-self.clamp_max_val, max=self.clamp_max_val)
return scores
class LayerScale(nn.Module):
"""LayerScale module as introduced in "Meta Pseudo Labels" and used in."""
def __init__(
self,
dim: int,
init_values: float | Tensor = 1e-5,
inplace: bool = False,
) -> None:
"""Initialize the LayerScale module."""
super().__init__()
self.inplace = inplace
self.gamma = nn.Parameter(init_values * torch.ones(dim))
def forward(self, x: Tensor) -> Tensor:
"""Apply LayerScale to the input tensor."""
return x.mul_(self.gamma) if self.inplace else x * self.gamma
class TransformerWrapper(nn.Module):
"""A wrapper for the transformer consisting of an encoder and a decoder."""
def __init__(
self,
encoder,
decoder,
d_model: int,
two_stage_type="none", # ["none"] only for now
pos_enc_at_input_dec=True,
):
"""Initialize the TransformerWrapper."""
super().__init__()
self.encoder = encoder
self.decoder = decoder
self.num_queries = decoder.num_queries if decoder is not None else None
self.pos_enc_at_input_dec = pos_enc_at_input_dec
# for two stage
assert two_stage_type in ["none"], f"unknown param {two_stage_type} of two_stage_type"
self.two_stage_type = two_stage_type
self._reset_parameters()
self.d_model = d_model
def _reset_parameters(self):
"""Initialize the parameters of the model."""
for n, p in self.named_parameters():
if p.dim() > 1:
if "box_embed" not in n and "query_embed" not in n and "reference_points" not in n:
nn.init.xavier_uniform_(p)
def get_valid_ratio(mask):
"""Compute the valid ratio of height and width from the mask."""
_, H, W = mask.shape
valid_H = torch.sum(~mask[:, :, 0], 1)
valid_W = torch.sum(~mask[:, 0, :], 1)
valid_ratio_h = valid_H.float() / H
valid_ratio_w = valid_W.float() / W
valid_ratio = torch.stack([valid_ratio_w, valid_ratio_h], -1)
return valid_ratio
def gen_sineembed_for_position(pos_tensor: torch.Tensor, num_feats: int = 256):
"""Generate sinusoidal position embeddings for 2D or 4D coordinate tensors.
This function creates sinusoidal embeddings using sine and cosine functions at different frequencies, similar to the
positional encoding used in Transformer models. It supports both 2D position tensors (x, y) and 4D tensors (x, y, w,
h) for bounding box coordinates.
Args:
pos_tensor (torch.Tensor): Input position tensor of shape (n_query, bs, 2) for 2D coordinates or (n_query, bs,
4) for 4D coordinates (bounding boxes).
num_feats (int): Number of feature dimensions for the output embedding. Must be even. Defaults to 256.
Returns:
(torch.Tensor): Sinusoidal position embeddings of shape (n_query, bs, num_feats) for 2D input or (n_query, bs,
num_feats * 2) for 4D input.
Raises:
AssertionError: If num_feats is not even.
ValueError: If pos_tensor.size(-1) is not 2 or 4.
Examples:
>>> pos_2d = torch.rand(100, 8, 2) # 100 queries, batch size 8, 2D coordinates
>>> embeddings_2d = gen_sineembed_for_position(pos_2d, num_feats=256)
>>> embeddings_2d.shape
torch.Size([100, 8, 256])
>>> pos_4d = torch.rand(50, 4, 4) # 50 queries, batch size 4, 4D coordinates
>>> embeddings_4d = gen_sineembed_for_position(pos_4d, num_feats=128)
>>> embeddings_4d.shape
torch.Size([50, 4, 256])
"""
assert num_feats % 2 == 0
num_feats = num_feats // 2
# n_query, bs, _ = pos_tensor.size()
# sineembed_tensor = torch.zeros(n_query, bs, 256)
scale = 2 * math.pi
dim_t = torch.arange(num_feats, dtype=pos_tensor.dtype, device=pos_tensor.device)
dim_t = 10000 ** (2 * (torch.div(dim_t, 2, rounding_mode="floor")) / num_feats)
x_embed = pos_tensor[:, :, 0] * scale
y_embed = pos_tensor[:, :, 1] * scale
pos_x = x_embed[:, :, None] / dim_t
pos_y = y_embed[:, :, None] / dim_t
pos_x = torch.stack((pos_x[:, :, 0::2].sin(), pos_x[:, :, 1::2].cos()), dim=3).flatten(2)
pos_y = torch.stack((pos_y[:, :, 0::2].sin(), pos_y[:, :, 1::2].cos()), dim=3).flatten(2)
if pos_tensor.size(-1) == 2:
pos = torch.cat((pos_y, pos_x), dim=2)
elif pos_tensor.size(-1) == 4:
w_embed = pos_tensor[:, :, 2] * scale
pos_w = w_embed[:, :, None] / dim_t
pos_w = torch.stack((pos_w[:, :, 0::2].sin(), pos_w[:, :, 1::2].cos()), dim=3).flatten(2)
h_embed = pos_tensor[:, :, 3] * scale
pos_h = h_embed[:, :, None] / dim_t
pos_h = torch.stack((pos_h[:, :, 0::2].sin(), pos_h[:, :, 1::2].cos()), dim=3).flatten(2)
pos = torch.cat((pos_y, pos_x, pos_w, pos_h), dim=2)
else:
raise ValueError(f"Unknown pos_tensor shape(-1):{pos_tensor.size(-1)}")
return pos

View File

@@ -0,0 +1,129 @@
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
"""Necks are the interface between a vision backbone and the rest of the detection model."""
from __future__ import annotations
from copy import deepcopy
import torch
import torch.nn as nn
class Sam3DualViTDetNeck(nn.Module):
"""A neck that implements a simple FPN as in ViTDet, with support for dual necks (for SAM3 and SAM2)."""
def __init__(
self,
trunk: nn.Module,
position_encoding: nn.Module,
d_model: int,
scale_factors=(4.0, 2.0, 1.0, 0.5),
add_sam2_neck: bool = False,
):
"""
SimpleFPN neck a la ViTDet
(From detectron2, very lightly adapted)
It supports a "dual neck" setting, where we have two identical necks (for SAM3 and SAM2), with different weights.
:param trunk: the backbone
:param position_encoding: the positional encoding to use
:param d_model: the dimension of the model
"""
super().__init__()
self.trunk = trunk
self.position_encoding = position_encoding
self.convs = nn.ModuleList()
self.scale_factors = scale_factors
use_bias = True
dim: int = self.trunk.channel_list[-1]
for _, scale in enumerate(scale_factors):
current = nn.Sequential()
if scale == 4.0:
current.add_module(
"dconv_2x2_0",
nn.ConvTranspose2d(dim, dim // 2, kernel_size=2, stride=2),
)
current.add_module(
"gelu",
nn.GELU(),
)
current.add_module(
"dconv_2x2_1",
nn.ConvTranspose2d(dim // 2, dim // 4, kernel_size=2, stride=2),
)
out_dim = dim // 4
elif scale == 2.0:
current.add_module(
"dconv_2x2",
nn.ConvTranspose2d(dim, dim // 2, kernel_size=2, stride=2),
)
out_dim = dim // 2
elif scale == 1.0:
out_dim = dim
elif scale == 0.5:
current.add_module(
"maxpool_2x2",
nn.MaxPool2d(kernel_size=2, stride=2),
)
out_dim = dim
else:
raise NotImplementedError(f"scale_factor={scale} is not supported yet.")
current.add_module(
"conv_1x1",
nn.Conv2d(
in_channels=out_dim,
out_channels=d_model,
kernel_size=1,
bias=use_bias,
),
)
current.add_module(
"conv_3x3",
nn.Conv2d(
in_channels=d_model,
out_channels=d_model,
kernel_size=3,
padding=1,
bias=use_bias,
),
)
self.convs.append(current)
self.sam2_convs = None
if add_sam2_neck:
# Assumes sam2 neck is just a clone of the original neck
self.sam2_convs = deepcopy(self.convs)
def forward(
self, tensor_list: list[torch.Tensor]
) -> tuple[list[torch.Tensor], list[torch.Tensor], list[torch.Tensor] | None, list[torch.Tensor] | None]:
"""Get feature maps and positional encodings from the neck."""
xs = self.trunk(tensor_list)
x = xs[-1] # simpleFPN
sam3_out, sam3_pos = self.sam_forward_feature_levels(x, self.convs)
if self.sam2_convs is None:
return sam3_out, sam3_pos, None, None
sam2_out, sam2_pos = self.sam_forward_feature_levels(x, self.sam2_convs)
return sam3_out, sam3_pos, sam2_out, sam2_pos
def sam_forward_feature_levels(
self, x: torch.Tensor, convs: nn.ModuleList
) -> tuple[list[torch.Tensor], list[torch.Tensor]]:
"""Run neck convolutions and compute positional encodings for each feature level."""
outs, poss = [], []
for conv in convs:
feat = conv(x)
outs.append(feat)
poss.append(self.position_encoding(feat).to(feat.dtype))
return outs, poss
def set_imgsz(self, imgsz: list[int] = [1008, 1008]):
"""Set the image size for the trunk backbone."""
self.trunk.set_imgsz(imgsz)

View File

@@ -0,0 +1,339 @@
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
from __future__ import annotations
from copy import deepcopy
import torch
from ultralytics.nn.modules.utils import inverse_sigmoid
from ultralytics.utils.ops import xywh2xyxy
from ..modules.sam import SAM2Model
from .geometry_encoders import Prompt
from .vl_combiner import SAM3VLBackbone
def _update_out(out, out_name, out_value, auxiliary=True, update_aux=True):
"""Helper function to update output dictionary with main and auxiliary outputs."""
out[out_name] = out_value[-1] if auxiliary else out_value
if auxiliary and update_aux:
if "aux_outputs" not in out:
out["aux_outputs"] = [{} for _ in range(len(out_value) - 1)]
assert len(out["aux_outputs"]) == len(out_value) - 1
for aux_output, aux_value in zip(out["aux_outputs"], out_value[:-1]):
aux_output[out_name] = aux_value
class SAM3SemanticModel(torch.nn.Module):
"""SAM3 model for semantic segmentation with vision-language backbone."""
def __init__(
self,
backbone: SAM3VLBackbone,
transformer,
input_geometry_encoder,
segmentation_head=None,
num_feature_levels=1,
o2m_mask_predict=True,
dot_prod_scoring=None,
use_instance_query: bool = True,
multimask_output: bool = True,
use_act_checkpoint_seg_head: bool = True,
matcher=None,
use_dot_prod_scoring=True,
supervise_joint_box_scores: bool = False, # only relevant if using presence token/score
detach_presence_in_joint_score: bool = False, # only relevant if using presence token/score
separate_scorer_for_instance: bool = False,
num_interactive_steps_val: int = 0,
):
"""Initialize the SAM3SemanticModel."""
super().__init__()
self.backbone = backbone
self.geometry_encoder = input_geometry_encoder
self.transformer = transformer
self.hidden_dim = transformer.d_model
self.num_feature_levels = num_feature_levels
self.segmentation_head = segmentation_head
self.o2m_mask_predict = o2m_mask_predict
self.dot_prod_scoring = dot_prod_scoring
self.use_act_checkpoint_seg_head = use_act_checkpoint_seg_head
self.matcher = matcher
self.num_interactive_steps_val = num_interactive_steps_val
self.use_dot_prod_scoring = use_dot_prod_scoring
if self.use_dot_prod_scoring:
assert dot_prod_scoring is not None
self.dot_prod_scoring = dot_prod_scoring
self.instance_dot_prod_scoring = None
if separate_scorer_for_instance:
self.instance_dot_prod_scoring = deepcopy(dot_prod_scoring)
else:
self.class_embed = torch.nn.Linear(self.hidden_dim, 1)
self.instance_class_embed = None
if separate_scorer_for_instance:
self.instance_class_embed = deepcopy(self.class_embed)
self.supervise_joint_box_scores = supervise_joint_box_scores
self.detach_presence_in_joint_score = detach_presence_in_joint_score
# verify the number of queries for O2O and O2M
num_o2o_static = self.transformer.decoder.num_queries
num_o2m_static = self.transformer.decoder.num_o2m_queries
assert num_o2m_static == (num_o2o_static if self.transformer.decoder.dac else 0)
self.dac = self.transformer.decoder.dac
self.use_instance_query = use_instance_query
self.multimask_output = multimask_output
self.text_embeddings = {}
self.names = []
def _encode_prompt(
self,
img_feats,
img_pos_embeds,
vis_feat_sizes,
geometric_prompt,
visual_prompt_embed=None,
visual_prompt_mask=None,
prev_mask_pred=None,
):
"""Encode the geometric and visual prompts."""
if prev_mask_pred is not None:
img_feats = [img_feats[-1] + prev_mask_pred]
# Encode geometry
geo_feats, geo_masks = self.geometry_encoder(
geo_prompt=geometric_prompt,
img_feats=img_feats,
img_sizes=vis_feat_sizes,
img_pos_embeds=img_pos_embeds,
)
if visual_prompt_embed is None:
visual_prompt_embed = torch.zeros((0, *geo_feats.shape[1:]), device=geo_feats.device)
visual_prompt_mask = torch.zeros(
(*geo_masks.shape[:-1], 0),
device=geo_masks.device,
dtype=geo_masks.dtype,
)
prompt = torch.cat([geo_feats, visual_prompt_embed], dim=0)
prompt_mask = torch.cat([geo_masks, visual_prompt_mask], dim=1)
return prompt, prompt_mask
def _run_encoder(
self,
img_feats,
img_pos_embeds,
vis_feat_sizes,
prompt,
prompt_mask,
encoder_extra_kwargs: dict | None = None,
):
"""Run the transformer encoder."""
# Run the encoder
# make a copy of the image feature lists since the encoder may modify these lists in-place
memory = self.transformer.encoder(
src=img_feats.copy(),
src_key_padding_mask=None,
src_pos=img_pos_embeds.copy(),
prompt=prompt,
prompt_key_padding_mask=prompt_mask,
feat_sizes=vis_feat_sizes,
encoder_extra_kwargs=encoder_extra_kwargs,
)
encoder_out = {
# encoded image features
"encoder_hidden_states": memory["memory"],
"pos_embed": memory["pos_embed"],
"padding_mask": memory["padding_mask"],
"spatial_shapes": memory["spatial_shapes"],
"valid_ratios": memory["valid_ratios"],
"vis_feat_sizes": vis_feat_sizes,
# encoded text features (or other prompts)
"prompt_before_enc": prompt,
"prompt_after_enc": memory.get("memory_text", prompt),
"prompt_mask": prompt_mask,
}
return encoder_out
def _run_decoder(
self,
pos_embed,
memory,
src_mask,
out,
prompt,
prompt_mask,
encoder_out,
):
"""Run the transformer decoder."""
bs = memory.shape[1]
query_embed = self.transformer.decoder.query_embed.weight
tgt = query_embed.unsqueeze(1).repeat(1, bs, 1)
hs, reference_boxes, dec_presence_out, _ = self.transformer.decoder(
tgt=tgt,
memory=memory,
memory_key_padding_mask=src_mask,
pos=pos_embed,
reference_boxes=None,
spatial_shapes=encoder_out["spatial_shapes"],
valid_ratios=encoder_out["valid_ratios"],
tgt_mask=None,
memory_text=prompt,
text_attention_mask=prompt_mask,
apply_dac=False,
)
hs = hs.transpose(1, 2) # seq-first to batch-first
reference_boxes = reference_boxes.transpose(1, 2) # seq-first to batch-first
if dec_presence_out is not None:
# seq-first to batch-first
dec_presence_out = dec_presence_out.transpose(1, 2)
self._update_scores_and_boxes(
out,
hs,
reference_boxes,
prompt,
prompt_mask,
dec_presence_out=dec_presence_out,
)
return out, hs
def _update_scores_and_boxes(
self,
out,
hs,
reference_boxes,
prompt,
prompt_mask,
dec_presence_out=None,
is_instance_prompt=False,
):
"""Update output dict with class scores and box predictions."""
num_o2o = hs.size(2)
# score prediction
if self.use_dot_prod_scoring:
dot_prod_scoring_head = self.dot_prod_scoring
if is_instance_prompt and self.instance_dot_prod_scoring is not None:
dot_prod_scoring_head = self.instance_dot_prod_scoring
outputs_class = dot_prod_scoring_head(hs, prompt, prompt_mask)
else:
class_embed_head = self.class_embed
if is_instance_prompt and self.instance_class_embed is not None:
class_embed_head = self.instance_class_embed
outputs_class = class_embed_head(hs)
# box prediction
box_head = self.transformer.decoder.bbox_embed
if is_instance_prompt and self.transformer.decoder.instance_bbox_embed is not None:
box_head = self.transformer.decoder.instance_bbox_embed
anchor_box_offsets = box_head(hs)
reference_boxes_inv_sig = inverse_sigmoid(reference_boxes)
outputs_coord = (reference_boxes_inv_sig + anchor_box_offsets).sigmoid()
outputs_boxes_xyxy = xywh2xyxy(outputs_coord)
if dec_presence_out is not None:
_update_out(out, "presence_logit_dec", dec_presence_out, update_aux=False)
if self.supervise_joint_box_scores:
assert dec_presence_out is not None
prob_dec_presence_out = dec_presence_out.clone().sigmoid()
if self.detach_presence_in_joint_score:
prob_dec_presence_out = prob_dec_presence_out.detach()
outputs_class = inverse_sigmoid(outputs_class.sigmoid() * prob_dec_presence_out.unsqueeze(2)).clamp(
min=-10.0, max=10.0
)
_update_out(out, "pred_logits", outputs_class[:, :, :num_o2o], update_aux=False)
_update_out(out, "pred_boxes", outputs_coord[:, :, :num_o2o], update_aux=False)
_update_out(out, "pred_boxes_xyxy", outputs_boxes_xyxy[:, :, :num_o2o], update_aux=False)
def _run_segmentation_heads(
self,
out,
backbone_out,
encoder_hidden_states,
prompt,
prompt_mask,
hs,
):
"""Run segmentation heads and get masks."""
if self.segmentation_head is not None:
num_o2o = hs.size(2)
obj_queries = hs if self.o2m_mask_predict else hs[:, :, :num_o2o]
seg_head_outputs = self.segmentation_head(
backbone_feats=backbone_out["backbone_fpn"],
obj_queries=obj_queries,
encoder_hidden_states=encoder_hidden_states,
prompt=prompt,
prompt_mask=prompt_mask,
)
for k, v in seg_head_outputs.items():
if k in self.segmentation_head.instance_keys:
_update_out(out, k, v[:, :num_o2o], auxiliary=False)
else:
out[k] = v
else:
backbone_out.pop("backbone_fpn", None)
def forward_grounding(
self, backbone_out: dict[str, torch.Tensor], text_ids: torch.Tensor, geometric_prompt: Prompt = None
):
"""Forward pass for grounding (detection + segmentation) given input images and text."""
backbone_out, img_feats, img_pos_embeds, vis_feat_sizes = SAM2Model._prepare_backbone_features(
self, backbone_out, batch=len(text_ids)
)
backbone_out.update({k: v for k, v in self.text_embeddings.items()})
with torch.profiler.record_function("SAM3Image._encode_prompt"):
prompt, prompt_mask = self._encode_prompt(img_feats, img_pos_embeds, vis_feat_sizes, geometric_prompt)
# index text features (note that regardless of early or late fusion, the batch size of
# `txt_feats` is always the number of *prompts* in the encoder)
txt_feats = backbone_out["language_features"][:, text_ids]
txt_masks = backbone_out["language_mask"][text_ids]
# encode text
prompt = torch.cat([txt_feats, prompt], dim=0)
prompt_mask = torch.cat([txt_masks, prompt_mask], dim=1)
# Run the encoder
with torch.profiler.record_function("SAM3Image._run_encoder"):
encoder_out = self._run_encoder(img_feats, img_pos_embeds, vis_feat_sizes, prompt, prompt_mask)
out = {"backbone_out": backbone_out}
# Run the decoder
with torch.profiler.record_function("SAM3Image._run_decoder"):
out, hs = self._run_decoder(
memory=encoder_out["encoder_hidden_states"],
pos_embed=encoder_out["pos_embed"],
src_mask=encoder_out["padding_mask"],
out=out,
prompt=prompt,
prompt_mask=prompt_mask,
encoder_out=encoder_out,
)
# Run segmentation heads
with torch.profiler.record_function("SAM3Image._run_segmentation_heads"):
self._run_segmentation_heads(
out=out,
backbone_out=backbone_out,
encoder_hidden_states=encoder_out["encoder_hidden_states"],
prompt=prompt,
prompt_mask=prompt_mask,
hs=hs,
)
return out
def set_classes(self, text: list[str]):
"""Set the text embeddings for the given class names."""
self.text_embeddings = self.backbone.forward_text(text)
self.names = text
def set_imgsz(self, imgsz: tuple[int, int]):
"""Set the image size for the model."""
self.backbone.set_imgsz(imgsz)

View File

@@ -0,0 +1,307 @@
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
from __future__ import annotations
from collections import OrderedDict
from typing import Callable
import torch
import torch.nn as nn
from torch.utils.checkpoint import checkpoint
from .model_misc import LayerScale
class ResidualAttentionBlock(nn.Module):
"""Transformer block with multi-head attention, layer normalization, and MLP feed-forward network."""
def __init__(
self,
d_model: int,
n_head: int,
mlp_ratio: float = 4.0,
ls_init_value: float | None = None,
act_layer: Callable[[], nn.Module] = nn.GELU,
norm_layer: Callable[[int], nn.Module] = nn.LayerNorm,
):
"""Initialize residual attention block with configurable dimensions and normalization."""
super().__init__()
# Attention
self.attn = nn.MultiheadAttention(d_model, n_head, batch_first=True)
# LayerNorm, LayerScale
self.ln_1 = norm_layer(d_model)
self.ln_2 = norm_layer(d_model)
self.ls_1 = LayerScale(d_model, ls_init_value) if ls_init_value is not None else nn.Identity()
self.ls_2 = LayerScale(d_model, ls_init_value) if ls_init_value is not None else nn.Identity()
# MLP
mlp_width = int(d_model * mlp_ratio)
self.mlp = nn.Sequential(
OrderedDict(
[
("c_fc", nn.Linear(d_model, mlp_width)),
("gelu", act_layer()),
("c_proj", nn.Linear(mlp_width, d_model)),
]
)
)
def attention(
self, q_x: torch.Tensor, k_x: torch.Tensor = None, v_x: torch.Tensor = None, attn_mask: torch.Tensor = None
) -> torch.Tensor:
"""Compute multi-head attention with optional cross-attention support and masking."""
k_x = k_x if k_x is not None else q_x
v_x = v_x if v_x is not None else q_x
if attn_mask is not None:
# Leave boolean masks as is
if not attn_mask.dtype == torch.bool:
attn_mask = attn_mask.to(q_x.dtype)
return self.attn(q_x, k_x, v_x, need_weights=False, attn_mask=attn_mask)[0]
def forward(
self, q_x: torch.Tensor, k_x: torch.Tensor = None, v_x: torch.Tensor = None, attn_mask: torch.Tensor = None
) -> torch.Tensor:
"""Apply residual attention with layer normalization and MLP, supporting optional cross-attention."""
k_x = self.ln_1_kv(k_x) if hasattr(self, "ln_1_kv") and k_x is not None else None
v_x = self.ln_1_kv(v_x) if hasattr(self, "ln_1_kv") and v_x is not None else None
x = q_x + self.ls_1(self.attention(q_x=self.ln_1(q_x), k_x=k_x, v_x=v_x, attn_mask=attn_mask))
x = x + self.ls_2(self.mlp(self.ln_2(x)))
return x
class Transformer(nn.Module):
"""Stack of residual attention blocks forming a transformer encoder with optional gradient checkpointing."""
def __init__(
self,
width: int,
layers: int,
heads: int,
mlp_ratio: float = 4.0,
ls_init_value: float | None = None,
act_layer: Callable[[], nn.Module] = nn.GELU,
norm_layer: Callable[[int], nn.Module] = nn.LayerNorm,
compile_mode: str | None = None,
use_act_checkpoint: bool = False,
):
"""Initialize transformer with configurable depth, width, and optional compilation/checkpointing."""
super().__init__()
self.width = width
self.layers = layers
self.grad_checkpointing = use_act_checkpoint
self.resblocks = nn.ModuleList(
[
ResidualAttentionBlock(
width,
heads,
mlp_ratio,
ls_init_value=ls_init_value,
act_layer=act_layer,
norm_layer=norm_layer,
)
for _ in range(layers)
]
)
if compile_mode is not None:
self.forward = torch.compile(self.forward, mode=compile_mode, fullgraph=True)
if self.grad_checkpointing:
torch._dynamo.config.optimize_ddp = False
def forward(self, x: torch.Tensor, attn_mask: torch.Tensor = None) -> torch.Tensor:
"""Process input through all transformer blocks with optional gradient checkpointing during training."""
for _, r in enumerate(self.resblocks):
if self.grad_checkpointing and not torch.jit.is_scripting() and self.training:
x = checkpoint(r, x, None, None, attn_mask, use_reentrant=False)
else:
x = r(x, attn_mask=attn_mask)
return x
def text_global_pool(
x: torch.Tensor, text: torch.Tensor = None, pool_type: str = "argmax"
) -> tuple[torch.Tensor, torch.Tensor]:
"""Extract pooled representation and tokens from text embeddings using specified pooling strategy
(first/last/argmax/none).
"""
if pool_type == "first":
pooled, tokens = x[:, 0], x[:, 1:]
elif pool_type == "last":
pooled, tokens = x[:, -1], x[:, :-1]
elif pool_type == "argmax":
# take features from the eot embedding (eot_token is the highest number in each sequence)
assert text is not None
pooled, tokens = x[torch.arange(x.shape[0]), text.argmax(dim=-1)], x
else:
pooled = tokens = x
return pooled, tokens
class TextTransformer(nn.Module):
"""Text transformer encoder with causal masking and flexible pooling strategies."""
def __init__(
self,
context_length: int = 77,
vocab_size: int = 49408,
width: int = 512,
heads: int = 8,
layers: int = 12,
mlp_ratio: float = 4.0,
ls_init_value: float | None = None,
output_dim: int = 512,
no_causal_mask: bool = False,
pool_type: str = "none", # no pooling
proj_bias: bool = False,
act_layer: Callable = nn.GELU,
norm_layer: Callable = nn.LayerNorm,
output_tokens: bool = False,
use_ln_post: bool = True,
compile_mode: str | None = None,
use_act_checkpoint: bool = False,
):
"""Initialize text transformer with embedding layers, transformer blocks, and pooling options."""
super().__init__()
assert pool_type in ("first", "last", "argmax", "none")
self.output_tokens = output_tokens
self.num_pos = self.context_length = context_length
self.vocab_size = vocab_size
self.width = width
self.output_dim = output_dim
self.heads = heads
self.pool_type = pool_type
self.token_embedding = nn.Embedding(self.vocab_size, width)
self.positional_embedding = nn.Parameter(torch.empty(self.num_pos, width))
self.transformer = Transformer(
width=width,
layers=layers,
heads=heads,
mlp_ratio=mlp_ratio,
ls_init_value=ls_init_value,
act_layer=act_layer,
norm_layer=norm_layer,
compile_mode=compile_mode,
use_act_checkpoint=use_act_checkpoint,
)
self.ln_final = norm_layer(width) if use_ln_post else nn.Identity()
if no_causal_mask:
self.attn_mask = None
else:
self.register_buffer("attn_mask", self.build_causal_mask(), persistent=False)
if proj_bias:
self.text_projection = nn.Linear(width, output_dim)
else:
self.text_projection = nn.Parameter(torch.empty(width, output_dim))
def build_causal_mask(self) -> torch.Tensor:
"""Create a causal attention mask to prevent attention to future tokens."""
# lazily create causal attention mask, with full attention between the tokens
# pytorch uses additive attention mask; fill with -inf
mask = torch.empty(self.num_pos, self.num_pos)
mask.fill_(float("-inf"))
mask.triu_(1) # zero out the lower diagonal
return mask
def forward(self, text: torch.Tensor) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
"""Forward pass through the text transformer, returning pooled output and optionally token embeddings."""
seq_len = text.shape[1]
x = self.token_embedding(text) # [batch_size, n_ctx, d_model]
attn_mask = self.attn_mask
if attn_mask is not None:
attn_mask = attn_mask[:seq_len, :seq_len]
x = x + self.positional_embedding[:seq_len]
x = self.transformer(x, attn_mask=attn_mask)
x = self.ln_final(x)
pooled, tokens = text_global_pool(x, text, pool_type=self.pool_type)
if self.text_projection is not None:
if isinstance(self.text_projection, nn.Linear):
pooled = self.text_projection(pooled)
else:
pooled = pooled @ self.text_projection
if self.output_tokens:
return pooled, tokens
return pooled
class VETextEncoder(nn.Module):
"""Text encoder for Vision Encoder (VE) models, combining a text transformer and a linear resizer."""
def __init__(
self,
d_model: int,
tokenizer: Callable,
width: int = 1024,
heads: int = 16,
layers: int = 24,
context_length: int = 32,
vocab_size: int = 49408,
use_ln_post: bool = True,
compile_mode: str | None = None,
use_act_checkpoint: bool = True,
):
"""Initialize VE text encoder with a text transformer and a linear resizer to match decoder dimensions."""
super().__init__()
self.context_length = context_length
self.use_ln_post = use_ln_post
self.tokenizer = tokenizer
self.encoder = TextTransformer(
context_length=self.context_length,
vocab_size=vocab_size,
width=width,
heads=heads,
layers=layers,
# we want the tokens, not just the pooled output
output_tokens=True,
use_ln_post=use_ln_post,
compile_mode=compile_mode,
use_act_checkpoint=use_act_checkpoint,
)
self.resizer = nn.Linear(self.encoder.width, d_model)
def forward(
self, text: list[str] | tuple[torch.Tensor, torch.Tensor, dict], input_boxes: list | None = None
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
"""Encode text input, either raw strings or pre-encoded tensors, and resize to match decoder dimensions."""
if isinstance(text[0], str):
# no use case for this
assert input_boxes is None or len(input_boxes) == 0, "not supported"
# Encode the text
tokenized = self.tokenizer(text, context_length=self.context_length).to(
self.resizer.weight.device
) # [b, seq_len]
text_attention_mask = (tokenized != 0).bool()
# manually embed the tokens
inputs_embeds = self.encoder.token_embedding(tokenized) # [b, seq_len, d=1024]
_, text_memory = self.encoder(tokenized) # [b, seq_len, d=1024]
assert text_memory.shape[1] == inputs_embeds.shape[1]
# Invert attention mask because its the opposite in pytorch transformer
text_attention_mask = text_attention_mask.ne(1)
# Transpose memory because pytorch's attention expects sequence first
text_memory = text_memory.transpose(0, 1)
# Resize the encoder hidden states to be of the same d_model as the decoder
text_memory_resized = self.resizer(text_memory)
else:
# The text is already encoded, use as is.
text_attention_mask, text_memory_resized, tokenized = text
inputs_embeds = tokenized["inputs_embeds"]
assert input_boxes is None or len(input_boxes) == 0, "Can't replace boxes in text if it's already encoded"
# Note that the input_embeds are returned in pytorch's convention (sequence first)
return (
text_attention_mask,
text_memory_resized,
inputs_embeds.transpose(0, 1),
)

View File

@@ -0,0 +1,547 @@
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
"""
ViTDet backbone adapted from Detectron2.
This module implements Vision Transformer (ViT) backbone for object detection.
Rope embedding code adopted from:
1. https://github.com/meta-llama/codellama/blob/main/llama/model.py
2. https://github.com/naver-ai/rope-vit
3. https://github.com/lucidrains/rotary-embedding-torch
"""
from __future__ import annotations
import math
from functools import partial
from typing import Callable
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.utils.checkpoint as checkpoint
from torch import Tensor
from ultralytics.models.sam.modules.blocks import PatchEmbed
from ultralytics.models.sam.modules.utils import (
apply_rotary_enc,
compute_axial_cis,
concat_rel_pos,
get_abs_pos,
window_partition,
window_unpartition,
)
from ultralytics.utils.checks import check_requirements
from .model_misc import LayerScale
class Attention(nn.Module):
"""Multi-head Attention block with relative position embeddings and 2d-rope."""
def __init__(
self,
dim: int,
num_heads: int = 8,
qkv_bias: bool = True,
use_rel_pos: bool = False,
rel_pos_zero_init: bool = True,
input_size: tuple[int, int] | None = None,
cls_token: bool = False,
use_rope: bool = False,
rope_theta: float = 10000.0,
rope_pt_size: tuple[int, int] | None = None,
rope_interp: bool = False,
):
"""
Args:
dim (int): Number of input channels.
num_heads (int): Number of attention heads.
qkv_bias (bool): If True, add a learnable bias to query, key, value.
rel_pos (bool): If True, add relative positional embeddings to the attention map.
rel_pos_zero_init (bool): If True, zero initialize relative positional parameters.
input_size (int or None): Input resolution for calculating the relative positional parameter size or rope
size.
attn_type: Type of attention operation, e.g. "vanilla", "vanilla-xformer".
cls_token: whether a cls_token is present.
use_rope: whether to use rope 2d (indep of use_rel_pos, as it can be used together)
use_rel_pos: whether to use relative positional embeddings
rope_theta: control frequencies of rope
rope_pt_size: size of rope in previous stage of training, needed for interpolation or tiling
rope_interp: whether to interpolate (or extrapolate) rope to match input size.
"""
super().__init__()
self.num_heads = num_heads
self.head_dim = dim // num_heads
self.scale = self.head_dim**-0.5
self.cls_token = cls_token
self.qkv = nn.Linear(dim, dim * 3, bias=qkv_bias)
self.proj = nn.Linear(dim, dim)
# rel_pos embeddings and rope
self.use_rel_pos = use_rel_pos
self.input_size = input_size
self.use_rope = use_rope
self.rope_theta = rope_theta
self.rope_pt_size = rope_pt_size
self.rope_interp = rope_interp
# init rel_pos embeddings and rope
self._setup_rel_pos(rel_pos_zero_init, input_size)
self._setup_rope_freqs(input_size)
def _setup_rel_pos(self, rel_pos_zero_init: bool = True, input_size: tuple[int, int] | None = None) -> None:
"""Setup relative positional embeddings."""
if not self.use_rel_pos:
self.rel_pos_h = None
self.rel_pos_w = None
return
assert input_size is not None
assert self.cls_token is False, "not supported"
# initialize relative positional embeddings
self.rel_pos_h = nn.Parameter(torch.zeros(2 * input_size[0] - 1, self.head_dim))
self.rel_pos_w = nn.Parameter(torch.zeros(2 * input_size[1] - 1, self.head_dim))
if not rel_pos_zero_init:
nn.init.trunc_normal_(self.rel_pos_h, std=0.02)
nn.init.trunc_normal_(self.rel_pos_w, std=0.02)
# Precompute the relative coords
H, W = input_size
q_coords = torch.arange(H)[:, None]
k_coords = torch.arange(W)[None, :]
relative_coords = (q_coords - k_coords) + (H - 1)
self.relative_coords = relative_coords.long()
def _setup_rope_freqs(self, input_size: tuple[int, int] | None = None) -> None:
"""Setup 2d-rope frequencies."""
if not self.use_rope:
self.freqs_cis = None
return
assert input_size is not None
# determine rope input size
if self.rope_pt_size is None:
self.rope_pt_size = input_size
# initialize 2d rope freqs
self.compute_cis = partial(
compute_axial_cis,
dim=self.head_dim,
theta=self.rope_theta,
)
# interpolate rope
scale_pos = 1.0
if self.rope_interp:
scale_pos = self.rope_pt_size[0] / input_size[0]
# get scaled freqs_cis
freqs_cis = self.compute_cis(
end_x=input_size[0],
end_y=input_size[1],
scale_pos=scale_pos,
)
if self.cls_token:
t = torch.zeros(
self.head_dim // 2,
dtype=torch.float32,
device=freqs_cis.device,
)
cls_freqs_cis = torch.polar(torch.ones_like(t), t)[None, :]
freqs_cis = torch.cat([cls_freqs_cis, freqs_cis], dim=0)
self.freqs_cis = freqs_cis
def _apply_rope(self, q, k) -> tuple[Tensor, Tensor]:
"""Apply 2d-rope to q and k."""
if not self.use_rope:
return q, k
assert self.freqs_cis is not None
return apply_rotary_enc(q, k, freqs_cis=self.freqs_cis.to(q.device))
def forward(self, x: Tensor) -> Tensor:
"""Forward pass of attention block."""
s = 1 if self.cls_token else 0 # used to exclude cls_token
if x.ndim == 4:
B, H, W, _ = x.shape
assert s == 0 # no cls_token
L = H * W
ndim = 4
else:
assert x.ndim == 3
B, L, _ = x.shape
ndim = 3
H = W = math.sqrt(L - s)
# qkv with shape (3, B, nHead, L, C)
qkv = self.qkv(x).reshape(B, L, 3, self.num_heads, -1)
# q, k, v with shape (B, nHead, L, C)
q, k, v = qkv.permute(2, 0, 3, 1, 4).unbind(0)
# handle rope and rel pos embeddings
q, k = self._apply_rope(q, k)
if self.use_rel_pos:
q, k = concat_rel_pos(
q.flatten(0, 1),
k.flatten(0, 1),
(H, W),
x.shape[1:3],
self.rel_pos_h,
self.rel_pos_w,
rescale=True,
relative_coords=self.relative_coords,
)
# sdpa expects [B, nheads, H*W, C] so we transpose back
q = q.reshape(B, self.num_heads, H * W, -1)
k = k.reshape(B, self.num_heads, H * W, -1)
x = F.scaled_dot_product_attention(q, k, v)
if ndim == 4:
x = x.view(B, self.num_heads, H, W, -1).permute(0, 2, 3, 1, 4).reshape(B, H, W, -1)
else:
x = x.view(B, self.num_heads, L, -1).permute(0, 2, 1, 3).reshape(B, L, -1)
x = self.proj(x)
return x
class Block(nn.Module):
"""Transformer blocks with support of window attention."""
def __init__(
self,
dim: int,
num_heads: int,
mlp_ratio: float = 4.0,
qkv_bias: bool = True,
drop_path: float = 0.0,
norm_layer: Callable[..., nn.Module] = nn.LayerNorm,
act_layer: Callable[..., nn.Module] = nn.GELU,
use_rel_pos: bool = False,
rel_pos_zero_init: bool = True,
window_size: int = 0,
input_size: tuple[int, int] | None = None,
use_rope: bool = False,
rope_pt_size: tuple[int, int] | None = None,
rope_interp: bool = False,
cls_token: bool = False,
dropout: float = 0.0,
init_values: float | None = None,
):
"""
Args:
dim (int): Number of input channels.
num_heads (int): Number of attention heads in each ViT block.
mlp_ratio (float): Ratio of mlp hidden dim to embedding dim.
qkv_bias (bool): If True, add a learnable bias to query, key, value.
drop_path (float): Stochastic depth rate.
norm_layer (nn.Module): Normalization layer.
act_layer (nn.Module): Activation layer.
use_rel_pos (bool): If True, add relative positional embeddings to the attention map.
rel_pos_zero_init (bool): If True, zero initialize relative positional parameters.
window_size (int): Window size for window attention blocks. If it equals 0, then not use window attention.
input_size (int or None): Input resolution for calculating the relative positional parameter size.
dropout (float): Dropout rate.
cls_token: whether a cls_token is present.
use_rope: whether to use rope 2d (indep of use_rel_pos, as it can be used together)
rope_pt_size: size of rope in previous stage of training, needed for interpolation or tiling
rope_interp: whether to interpolate (or extrapolate) rope to match target input size, expected to specify
source size as rope_pt_size.
init_values: layer scale init, None for no layer scale.
"""
super().__init__()
check_requirements("timm")
from timm.layers import DropPath, Mlp
self.norm1 = norm_layer(dim)
self.attn = Attention(
dim,
num_heads=num_heads,
qkv_bias=qkv_bias,
use_rel_pos=use_rel_pos,
rel_pos_zero_init=rel_pos_zero_init,
input_size=input_size if window_size == 0 else (window_size, window_size),
use_rope=use_rope,
rope_pt_size=rope_pt_size,
rope_interp=rope_interp,
cls_token=cls_token,
)
self.ls1 = LayerScale(dim, init_values=init_values) if init_values else nn.Identity()
self.drop_path = DropPath(drop_path) if drop_path > 0.0 else nn.Identity()
self.norm2 = norm_layer(dim)
self.mlp = Mlp(
in_features=dim,
hidden_features=int(dim * mlp_ratio),
act_layer=act_layer,
drop=(dropout, 0.0),
)
self.ls2 = LayerScale(dim, init_values=init_values) if init_values else nn.Identity()
self.dropout = nn.Dropout(dropout)
self.window_size = window_size
def forward(self, x: Tensor) -> Tensor:
"""Forward pass of the transformer block."""
shortcut = x
x = self.norm1(x)
# Window partition
if self.window_size > 0:
H, W = x.shape[1], x.shape[2]
x, pad_hw = window_partition(x, self.window_size)
x = self.ls1(self.attn(x))
# Reverse window partition
if self.window_size > 0:
x = window_unpartition(x, self.window_size, pad_hw, (H, W))
x = shortcut + self.dropout(self.drop_path(x))
x = x + self.dropout(self.drop_path(self.ls2(self.mlp(self.norm2(x)))))
return x
class ViT(nn.Module):
"""This module implements Vision Transformer (ViT) backbone in :paper:`vitdet`. "Exploring Plain Vision Transformer
Backbones for Object Detection", https://arxiv.org/abs/2203.16527.
"""
def __init__(
self,
img_size: int = 1024,
patch_size: int = 16,
in_chans: int = 3,
embed_dim: int = 768,
depth: int = 12,
num_heads: int = 12,
mlp_ratio: float = 4.0,
qkv_bias: bool = True,
drop_path_rate: float = 0.0,
norm_layer: Callable[..., nn.Module] | str = "LayerNorm",
act_layer: Callable[..., nn.Module] = nn.GELU,
use_abs_pos: bool = True,
tile_abs_pos: bool = True,
rel_pos_blocks: tuple[int, ...] | bool = (2, 5, 8, 11),
rel_pos_zero_init: bool = True,
window_size: int = 14,
global_att_blocks: tuple[int, ...] = (2, 5, 8, 11),
use_rope: bool = False,
rope_pt_size: int | None = None,
use_interp_rope: bool = False,
pretrain_img_size: int = 224,
pretrain_use_cls_token: bool = True,
retain_cls_token: bool = True,
dropout: float = 0.0,
return_interm_layers: bool = False,
init_values: float | None = None, # for layerscale
ln_pre: bool = False,
ln_post: bool = False,
bias_patch_embed: bool = True,
compile_mode: str | None = None,
use_act_checkpoint: bool = True,
):
"""
Args:
img_size (int): Input image size. Only relevant for rel pos or rope.
patch_size (int): Patch size.
in_chans (int): Number of input image channels.
embed_dim (int): Patch embedding dimension.
depth (int): Depth of ViT.
num_heads (int): Number of attention heads in each ViT block.
mlp_ratio (float): Ratio of mlp hidden dim to embedding dim.
qkv_bias (bool): If True, add a learnable bias to query, key, value.
drop_path_rate (float): Stochastic depth rate.
norm_layer (nn.Module): Normalization layer.
act_layer (nn.Module): Activation layer.
use_abs_pos (bool): If True, use absolute positional embeddings.
tile_abs_pos (bool): If True, tile absolute positional embeddings instead of interpolation.
rel_pos_blocks (list): Blocks which have rel pos embeddings.
rel_pos_zero_init (bool): If True, zero initialize relative positional parameters.
window_size (int): Window size for window attention blocks.
global_att_blocks (list): Indexes for blocks using global attention (other blocks use window attention).
use_rope (bool): whether to use rope 2d (indep of rel_pos_blocks, as it can be used together).
rope_pt_size (int): size of rope in previous stage of training, needed for interpolation or tiling.
use_interp_rope: whether to interpolate (or extrapolate) rope to match target input size, expected to
specify source size as rope_pt_size.
use_act_checkpoint (bool): If True, use activation checkpointing.
pretrain_img_size (int): input image size for pretraining models.
pretrain_use_cls_token (bool): If True, pretraining models use class token.
retain_cls_token: whether cls_token should be retained.
dropout (float): Dropout rate. Applied in residual blocks of attn, mlp and inside the mlp.
return_interm_layers (bool): Whether to return intermediate layers (all global attention blocks).
init_values: layer scale init, None for no layer scale.
ln_pre (bool): If True, apply layer norm before transformer blocks.
ln_post (bool): If True, apply layer norm after transformer blocks.
bias_patch_embed (bool): bias in conv for patch embed?
compile_mode (str): mode to compile the forward.
"""
super().__init__()
self.pretrain_use_cls_token = pretrain_use_cls_token
window_block_indexes = [i for i in range(depth) if i not in global_att_blocks]
self.full_attn_ids = list(global_att_blocks)
self.rel_pos_blocks = [False] * depth
if isinstance(rel_pos_blocks, bool) and rel_pos_blocks:
self.rel_pos_blocks = [True] * depth
else:
for i in rel_pos_blocks:
self.rel_pos_blocks[i] = True
self.retain_cls_token = retain_cls_token
if self.retain_cls_token:
assert pretrain_use_cls_token
assert len(window_block_indexes) == 0, "windowing not supported with cls token"
assert sum(self.rel_pos_blocks) == 0, "rel pos not supported with cls token"
scale = embed_dim**-0.5
self.class_embedding = nn.Parameter(scale * torch.randn(1, 1, embed_dim))
if isinstance(norm_layer, str):
norm_layer = partial(getattr(nn, norm_layer), eps=1e-5)
self.patch_embed = PatchEmbed(
kernel_size=(patch_size, patch_size),
stride=(patch_size, patch_size),
in_chans=in_chans,
embed_dim=embed_dim,
bias=bias_patch_embed,
)
# Handle absolute positional embedding
self.tile_abs_pos = tile_abs_pos
self.use_abs_pos = use_abs_pos
if self.tile_abs_pos:
assert self.use_abs_pos
if self.use_abs_pos:
# Initialize absolute positional embedding with pretrain image size.
num_patches = (pretrain_img_size // patch_size) * (pretrain_img_size // patch_size)
num_positions = (num_patches + 1) if pretrain_use_cls_token else num_patches
self.pos_embed = nn.Parameter(torch.zeros(1, num_positions, embed_dim))
else:
self.pos_embed = None
# stochastic depth decay rule
dpr = [x.item() for x in torch.linspace(0, drop_path_rate, depth)]
self.patch_size = patch_size
self.window_size = window_size
self.blocks = nn.ModuleList()
cur_stage = 1
for i in range(depth):
block = Block(
dim=embed_dim,
num_heads=num_heads,
mlp_ratio=mlp_ratio,
qkv_bias=qkv_bias,
drop_path=dpr[i],
norm_layer=norm_layer,
act_layer=act_layer,
use_rel_pos=self.rel_pos_blocks[i],
rel_pos_zero_init=rel_pos_zero_init,
window_size=window_size if i in window_block_indexes else 0,
input_size=(img_size // patch_size, img_size // patch_size),
use_rope=use_rope,
rope_pt_size=((window_size, window_size) if rope_pt_size is None else (rope_pt_size, rope_pt_size)),
rope_interp=use_interp_rope,
cls_token=self.retain_cls_token,
dropout=dropout,
init_values=init_values,
)
if i not in window_block_indexes:
cur_stage += 1
self.use_act_checkpoint = use_act_checkpoint
self.blocks.append(block)
self.return_interm_layers = return_interm_layers
self.channel_list = [embed_dim] * len(self.full_attn_ids) if return_interm_layers else [embed_dim]
if self.pos_embed is not None:
nn.init.trunc_normal_(self.pos_embed, std=0.02)
self.ln_pre = norm_layer(embed_dim) if ln_pre else nn.Identity()
self.ln_post = norm_layer(embed_dim) if ln_post else nn.Identity()
self.apply(self._init_weights)
if compile_mode is not None:
self.forward = torch.compile(self.forward, mode=compile_mode, fullgraph=True)
if self.use_act_checkpoint and self.training:
torch._dynamo.config.optimize_ddp = False
@staticmethod
def _init_weights(m: nn.Module) -> None:
"""Initialize the weights."""
if isinstance(m, nn.Linear):
nn.init.trunc_normal_(m.weight, std=0.02)
if isinstance(m, nn.Linear) and m.bias is not None:
nn.init.constant_(m.bias, 0)
elif isinstance(m, nn.LayerNorm):
nn.init.constant_(m.bias, 0)
nn.init.constant_(m.weight, 1.0)
def forward(self, x: torch.Tensor) -> list[torch.Tensor]:
"""Vit forward path and get feature maps."""
x = self.patch_embed(x)
h, w = x.shape[1], x.shape[2]
s = 0
if self.retain_cls_token:
# If cls_token is retained, we don't
# maintain spatial shape
x = torch.cat([self.class_embedding, x.flatten(1, 2)], dim=1)
s = 1
if self.pos_embed is not None:
x = x + get_abs_pos(
self.pos_embed,
self.pretrain_use_cls_token,
(h, w),
self.retain_cls_token,
tiling=self.tile_abs_pos,
)
x = self.ln_pre(x)
outputs = []
for i, blk in enumerate(self.blocks):
if self.use_act_checkpoint and self.training:
x = checkpoint.checkpoint(blk, x, use_reentrant=False)
else:
x = blk(x)
if (i == self.full_attn_ids[-1]) or (self.return_interm_layers and i in self.full_attn_ids):
if i == self.full_attn_ids[-1]:
x = self.ln_post(x)
feats = x[:, s:]
if feats.ndim == 4:
feats = feats.permute(0, 3, 1, 2)
else:
assert feats.ndim == 3
h = w = math.sqrt(feats.shape[1])
feats = feats.reshape(feats.shape[0], h, w, feats.shape[-1]).permute(0, 3, 1, 2)
outputs.append(feats)
return outputs
def set_imgsz(self, imgsz: list[int] = [1008, 1008]):
"""Setup rel pos embeddings and rope freqs for a new input image size."""
for block in self.blocks:
if block.window_size != 0:
continue
block.attn._setup_rel_pos(input_size=(imgsz[0] // self.patch_size, imgsz[1] // self.patch_size))
block.attn._setup_rope_freqs(input_size=(imgsz[0] // self.patch_size, imgsz[1] // self.patch_size))

View File

@@ -0,0 +1,160 @@
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
"""Provides utility to combine a vision backbone with a language backbone."""
from __future__ import annotations
from copy import copy
import torch
import torch.nn as nn
from torch.nn.attention import SDPBackend, sdpa_kernel
from .necks import Sam3DualViTDetNeck
class SAM3VLBackbone(nn.Module):
"""This backbone combines a vision backbone and a language backbone without fusion. As such it is more of a
convenience wrapper to handle the two backbones together.
It adds support for activation checkpointing and compilation.
"""
def __init__(
self,
visual: Sam3DualViTDetNeck,
text,
compile_visual: bool = False,
act_ckpt_whole_vision_backbone: bool = False,
act_ckpt_whole_language_backbone: bool = False,
scalp=0,
):
"""Initialize the backbone combiner.
:param visual: The vision backbone to use
:param text: The text encoder to use
"""
super().__init__()
self.vision_backbone: Sam3DualViTDetNeck = torch.compile(visual) if compile_visual else visual
self.language_backbone = text
self.scalp = scalp
# allow running activation checkpointing on the entire vision and language backbones
self.act_ckpt_whole_vision_backbone = act_ckpt_whole_vision_backbone
self.act_ckpt_whole_language_backbone = act_ckpt_whole_language_backbone
def forward(
self,
samples: torch.Tensor,
captions: list[str],
input_boxes: torch.Tensor = None,
additional_text: list[str] | None = None,
):
"""Forward pass of the backbone combiner.
:param samples: The input images
:param captions: The input captions
:param input_boxes: If the text contains place-holders for boxes, this
parameter contains the tensor containing their spatial features
:param additional_text: This can be used to encode some additional text
(different from the captions) in the same forward of the backbone
:return: Output dictionary with the following keys:
- vision_features: The output of the vision backbone
- language_features: The output of the language backbone
- language_mask: The attention mask of the language backbone
- vision_pos_enc: The positional encoding of the vision backbone
- (optional) additional_text_features: The output of the language
backbone for the additional text
- (optional) additional_text_mask: The attention mask of the
language backbone for the additional text
"""
output = self.forward_image(samples)
output.update(self.forward_text(captions, input_boxes, additional_text))
return output
def forward_image(self, samples: torch.Tensor):
"""Forward pass of the vision backbone and get both SAM3 and SAM2 features."""
# Forward through backbone
sam3_features, sam3_pos, sam2_features, sam2_pos = self.vision_backbone.forward(samples)
if self.scalp > 0:
# Discard the lowest resolution features
sam3_features, sam3_pos = (
sam3_features[: -self.scalp],
sam3_pos[: -self.scalp],
)
if sam2_features is not None and sam2_pos is not None:
sam2_features, sam2_pos = (
sam2_features[: -self.scalp],
sam2_pos[: -self.scalp],
)
sam2_output = None
if sam2_features is not None and sam2_pos is not None:
sam2_src = sam2_features[-1]
sam2_output = {
"vision_features": sam2_src,
"vision_pos_enc": sam2_pos,
"backbone_fpn": sam2_features,
}
sam3_src = sam3_features[-1]
return {
"vision_features": sam3_src,
"vision_pos_enc": sam3_pos,
"backbone_fpn": sam3_features,
"sam2_backbone_out": sam2_output,
}
def forward_image_sam2(self, samples: torch.Tensor):
"""Forward pass of the vision backbone to get SAM2 features only."""
xs = self.vision_backbone.trunk(samples)
x = xs[-1] # simpleFPN
assert self.vision_backbone.sam2_convs is not None, "SAM2 neck is not available."
sam2_features, sam2_pos = self.vision_backbone.sam_forward_feature_levels(x, self.vision_backbone.sam2_convs)
if self.scalp > 0:
# Discard the lowest resolution features
sam2_features, sam2_pos = (
sam2_features[: -self.scalp],
sam2_pos[: -self.scalp],
)
return {
"vision_features": sam2_features[-1],
"vision_pos_enc": sam2_pos,
"backbone_fpn": sam2_features,
}
def forward_text(self, captions, input_boxes=None, additional_text=None):
"""Forward pass of the text encoder."""
output = {}
# Forward through text_encoder
text_to_encode = copy(captions)
if additional_text is not None:
# if there are additional_text, we piggy-back them into this forward.
# They'll be used later for output alignment
text_to_encode += additional_text
with sdpa_kernel([SDPBackend.MATH, SDPBackend.EFFICIENT_ATTENTION, SDPBackend.FLASH_ATTENTION]):
text_attention_mask, text_memory, text_embeds = self.language_backbone(text_to_encode, input_boxes)
if additional_text is not None:
output["additional_text_features"] = text_memory[:, -len(additional_text) :]
output["additional_text_mask"] = text_attention_mask[-len(additional_text) :]
text_memory = text_memory[:, : len(captions)]
text_attention_mask = text_attention_mask[: len(captions)]
text_embeds = text_embeds[:, : len(captions)]
output["language_features"] = text_memory
output["language_mask"] = text_attention_mask
output["language_embeds"] = text_embeds # Text embeddings before forward to the encoder
return output
def set_imgsz(self, imgsz: list[int] = [1008, 1008]):
"""Set the image size for the vision backbone."""
self.vision_backbone.set_imgsz(imgsz)