Gertlek's picture
Clean DetectiveSAMv2-only release
0b2df65 verified
Raw
History Blame Contribute Delete
26 kB
from __future__ import annotations
from typing import Optional
import torch
import torch.nn as nn
import torch.nn.functional as F
FeaturePyramid = list[torch.Tensor]
StreamPyramid = list[list[torch.Tensor]]
__all__ = [
"FeaturePyramid",
"FlexibleEvidenceBlock",
"FlexibleEvidenceProjector",
"SpatialCrossAttentionSharedAdapter",
"StreamEvidenceBuilder",
"StreamPyramid",
"Transformer2DBlock",
"TransformerEvidenceMaskAdapter",
]
def _group_count(channels: int, max_groups: int = 8) -> int:
for groups in range(min(max_groups, channels), 0, -1):
if channels % groups == 0:
return groups
return 1
class FlexibleEvidenceBlock(nn.Module):
def __init__(self, channels: int, dropout_rate: float = 0.1, expansion: int = 2) -> None:
super().__init__()
expanded = channels * expansion
self.norm = nn.GroupNorm(_group_count(channels), channels)
self.expand = nn.Conv2d(channels, expanded, kernel_size=1)
self.depthwise = nn.Conv2d(expanded, expanded, kernel_size=3, padding=1, groups=expanded)
self.act = nn.GELU()
self.channel_gate = nn.Sequential(
nn.AdaptiveAvgPool2d(1),
nn.Conv2d(expanded, max(channels // 4, 8), kernel_size=1),
nn.GELU(),
nn.Conv2d(max(channels // 4, 8), expanded, kernel_size=1),
nn.Sigmoid(),
)
self.project = nn.Conv2d(expanded, channels, kernel_size=1)
self.dropout = nn.Dropout2d(p=dropout_rate)
def forward(self, x: torch.Tensor) -> torch.Tensor:
residual = x
hidden = self.norm(x)
hidden = self.expand(hidden)
hidden = self.depthwise(hidden)
hidden = self.act(hidden)
hidden = hidden * self.channel_gate(hidden)
hidden = self.project(hidden)
hidden = self.dropout(hidden)
return residual + hidden
class FlexibleEvidenceProjector(nn.Module):
"""Channel and local-spatial evidence projector used by v2 adapters."""
def __init__(
self,
in_channels: int,
hidden_dim: int,
dropout_rate: float = 0.1,
depth: int = 2,
) -> None:
super().__init__()
self.input_proj = nn.Sequential(
nn.Conv2d(in_channels, hidden_dim, kernel_size=1),
nn.GroupNorm(_group_count(hidden_dim), hidden_dim),
nn.GELU(),
nn.Dropout2d(p=dropout_rate),
)
self.blocks = nn.Sequential(
*[
FlexibleEvidenceBlock(hidden_dim, dropout_rate=dropout_rate)
for _ in range(depth)
]
)
def forward(self, x: torch.Tensor) -> torch.Tensor:
return self.blocks(self.input_proj(x))
class StreamEvidenceBuilder(nn.Module):
"""Build target evidence from raw target and perturbation-stream features."""
def __init__(
self,
in_channels_list: list[int],
hidden_dim: int,
dropout_rate: float = 0.1,
max_streams: int = 3,
) -> None:
super().__init__()
self.max_streams = max(1, max_streams)
self.projections = nn.ModuleList(
[
FlexibleEvidenceProjector(
in_channels=channels * (1 + 3 * self.max_streams),
hidden_dim=hidden_dim,
dropout_rate=dropout_rate,
depth=3,
)
for channels in in_channels_list
]
)
def forward(
self,
unadapted: FeaturePyramid,
streams_unadapted: StreamPyramid,
) -> FeaturePyramid:
evidence = []
for scale_idx, (target, streams) in enumerate(zip(unadapted, streams_unadapted)):
fixed_streams = list(streams[: self.max_streams])
while len(fixed_streams) < self.max_streams:
fixed_streams.append(torch.zeros_like(target))
abs_diffs = [(target - stream).abs() for stream in fixed_streams]
signed_diffs = [target - stream for stream in fixed_streams]
raw = torch.cat([target, *fixed_streams, *abs_diffs, *signed_diffs], dim=1)
evidence.append(self.projections[scale_idx](raw))
return evidence
class CrossAttentionSharedAdapter(nn.Module):
"""Per-scale cross-attention adapter over perturbation streams."""
def __init__(
self,
in_channels_list: list[int],
hidden_dim: int,
dropout_rate: float = 0.1,
max_streams: int = 2,
num_heads: int = 4,
) -> None:
super().__init__()
self.num_scales = len(in_channels_list)
self.max_streams = max(1, max_streams)
self.num_heads = max(1, num_heads)
if hidden_dim % self.num_heads != 0:
raise ValueError(f"hidden_dim ({hidden_dim}) must be divisible by num_heads ({self.num_heads})")
self.head_dim = hidden_dim // self.num_heads
self.scale = self.head_dim**-0.5
self.q_proj = nn.ModuleList(nn.Conv2d(channels, hidden_dim, kernel_size=1) for channels in in_channels_list)
self.k_proj = nn.ModuleList(nn.Conv2d(channels, hidden_dim, kernel_size=1) for channels in in_channels_list)
self.v_proj = nn.ModuleList(nn.Conv2d(channels, hidden_dim, kernel_size=1) for channels in in_channels_list)
self.out_proj = nn.ModuleList(
[
nn.Sequential(
nn.Conv2d(hidden_dim, hidden_dim, kernel_size=1),
nn.GELU(),
nn.Dropout2d(p=dropout_rate),
nn.Conv2d(hidden_dim, channels, kernel_size=1),
)
for channels in in_channels_list
]
)
self.evidence_q_proj = nn.ModuleList(
nn.Conv2d(hidden_dim, hidden_dim, kernel_size=1)
for _ in in_channels_list
)
self.evidence_context_proj = nn.ModuleList(
[
nn.Sequential(
nn.Conv2d(hidden_dim, hidden_dim, kernel_size=1),
nn.GELU(),
nn.Dropout2d(p=dropout_rate),
)
for _ in in_channels_list
]
)
self.residual_gate = nn.ModuleList(
[
nn.Sequential(
nn.Conv2d(channels + hidden_dim, channels, kernel_size=1),
nn.Sigmoid(),
)
for channels in in_channels_list
]
)
self.dropout = nn.Dropout2d(p=dropout_rate)
def forward(
self,
stream_features: list[torch.Tensor],
unadapted: torch.Tensor,
scale_idx: int,
return_delta: bool = False,
evidence: Optional[torch.Tensor] = None,
) -> torch.Tensor:
if not stream_features:
delta = torch.zeros_like(unadapted)
adapted = unadapted
if return_delta:
return adapted, delta
return adapted
batch_size, _, height, width = unadapted.shape
stream_count = len(stream_features)
query = self.q_proj[scale_idx](unadapted)
if evidence is not None:
if evidence.shape[-2:] != query.shape[-2:]:
evidence = F.interpolate(evidence, size=query.shape[-2:], mode="bilinear", align_corners=False)
query = query + self.evidence_q_proj[scale_idx](evidence)
query = query.view(batch_size, self.num_heads, self.head_dim, 1, height, width)
keys = [self.k_proj[scale_idx](feature) for feature in stream_features]
values = [self.v_proj[scale_idx](feature) for feature in stream_features]
key = torch.stack(keys, dim=1).view(batch_size, stream_count, self.num_heads, self.head_dim, height, width)
value = torch.stack(values, dim=1).view(batch_size, stream_count, self.num_heads, self.head_dim, height, width)
key = key.permute(0, 2, 3, 1, 4, 5)
value = value.permute(0, 2, 3, 1, 4, 5)
attention_logits = (query * key).sum(dim=2) * self.scale
attention = F.softmax(attention_logits, dim=2)
context = (attention.unsqueeze(2) * value).sum(dim=3)
context = context.reshape(batch_size, self.num_heads * self.head_dim, height, width)
if evidence is not None:
context = context + self.evidence_context_proj[scale_idx](evidence)
context = self.dropout(context)
raw_delta = self.out_proj[scale_idx](context)
gate = self.residual_gate[scale_idx](torch.cat([unadapted, context], dim=1))
delta = gate * raw_delta
adapted = unadapted + delta
if return_delta:
return adapted, delta
return adapted
class SpatialCrossAttentionSharedAdapter(CrossAttentionSharedAdapter):
"""Cross-attention adapter with global low-res and windowed mid-res spatial mixing."""
def __init__(
self,
in_channels_list: list[int],
hidden_dim: int,
dropout_rate: float = 0.1,
max_streams: int = 2,
num_heads: int = 4,
global_spatial_scales: tuple[int, ...] = (0,),
windowed_spatial_scales: tuple[int, ...] = (2,),
window_size: int = 8,
) -> None:
super().__init__(
in_channels_list=in_channels_list,
hidden_dim=hidden_dim,
dropout_rate=dropout_rate,
max_streams=max_streams,
num_heads=num_heads,
)
self.global_spatial_scales = set(global_spatial_scales)
self.windowed_spatial_scales = set(windowed_spatial_scales)
self.window_size = window_size
def _apply_residual(
self,
context: torch.Tensor,
unadapted: torch.Tensor,
scale_idx: int,
return_delta: bool,
evidence: Optional[torch.Tensor] = None,
) -> torch.Tensor:
if evidence is not None:
if evidence.shape[-2:] != context.shape[-2:]:
evidence = F.interpolate(evidence, size=context.shape[-2:], mode="bilinear", align_corners=False)
context = context + self.evidence_context_proj[scale_idx](evidence)
context = self.dropout(context)
raw_delta = self.out_proj[scale_idx](context)
gate = self.residual_gate[scale_idx](torch.cat([unadapted, context], dim=1))
delta = gate * raw_delta
adapted = unadapted + delta
if return_delta:
return adapted, delta
return adapted
def _global_spatial_context(
self,
stream_features: list[torch.Tensor],
unadapted: torch.Tensor,
scale_idx: int,
evidence: Optional[torch.Tensor] = None,
) -> torch.Tensor:
batch_size, _, height, width = unadapted.shape
stream_count = len(stream_features)
num_tokens = height * width
query = self.q_proj[scale_idx](unadapted)
if evidence is not None:
if evidence.shape[-2:] != query.shape[-2:]:
evidence = F.interpolate(evidence, size=query.shape[-2:], mode="bilinear", align_corners=False)
query = query + self.evidence_q_proj[scale_idx](evidence)
query = query.view(batch_size, self.num_heads, self.head_dim, num_tokens).transpose(2, 3)
keys = [self.k_proj[scale_idx](feature) for feature in stream_features]
values = [self.v_proj[scale_idx](feature) for feature in stream_features]
key = torch.stack(keys, dim=1).view(batch_size, stream_count, self.num_heads, self.head_dim, num_tokens)
value = torch.stack(values, dim=1).view(batch_size, stream_count, self.num_heads, self.head_dim, num_tokens)
key = key.permute(0, 2, 1, 4, 3).reshape(batch_size, self.num_heads, stream_count * num_tokens, self.head_dim)
value = value.permute(0, 2, 1, 4, 3).reshape(batch_size, self.num_heads, stream_count * num_tokens, self.head_dim)
attention = torch.matmul(query, key.transpose(-1, -2)) * self.scale
attention = F.softmax(attention, dim=-1)
context = torch.matmul(attention, value)
return context.transpose(2, 3).reshape(batch_size, self.num_heads * self.head_dim, height, width)
def _window_partition_q(self, x: torch.Tensor, window_size: int) -> torch.Tensor:
batch_size, heads, head_dim, height, width = x.shape
return (
x.permute(0, 3, 4, 1, 2)
.reshape(batch_size, height // window_size, window_size, width // window_size, window_size, heads, head_dim)
.permute(0, 1, 3, 5, 2, 4, 6)
.reshape(batch_size * (height // window_size) * (width // window_size), heads, window_size * window_size, head_dim)
)
def _window_partition_kv(self, x: torch.Tensor, window_size: int) -> torch.Tensor:
batch_size, streams, heads, head_dim, height, width = x.shape
return (
x.permute(0, 4, 5, 1, 2, 3)
.reshape(
batch_size,
height // window_size,
window_size,
width // window_size,
window_size,
streams,
heads,
head_dim,
)
.permute(0, 1, 3, 6, 2, 4, 5, 7)
.reshape(
batch_size * (height // window_size) * (width // window_size),
heads,
window_size * window_size * streams,
head_dim,
)
)
def _window_unpartition(
self,
x: torch.Tensor,
batch_size: int,
height: int,
width: int,
window_size: int,
) -> torch.Tensor:
heads = x.shape[1]
head_dim = x.shape[-1]
windows_h = height // window_size
windows_w = width // window_size
return (
x.reshape(batch_size, windows_h, windows_w, heads, window_size, window_size, head_dim)
.permute(0, 3, 6, 1, 4, 2, 5)
.reshape(batch_size, heads, head_dim, height, width)
)
def _windowed_spatial_context(
self,
stream_features: list[torch.Tensor],
unadapted: torch.Tensor,
scale_idx: int,
evidence: Optional[torch.Tensor] = None,
) -> torch.Tensor:
batch_size, _, height, width = unadapted.shape
stream_count = len(stream_features)
window_size = self.window_size
pad_h = (window_size - height % window_size) % window_size
pad_w = (window_size - width % window_size) % window_size
padded_h = height + pad_h
padded_w = width + pad_w
query = self.q_proj[scale_idx](unadapted)
if evidence is not None:
if evidence.shape[-2:] != query.shape[-2:]:
evidence = F.interpolate(evidence, size=query.shape[-2:], mode="bilinear", align_corners=False)
query = query + self.evidence_q_proj[scale_idx](evidence)
query = query.view(batch_size, self.num_heads, self.head_dim, height, width)
keys = [
self.k_proj[scale_idx](feature).view(batch_size, self.num_heads, self.head_dim, height, width)
for feature in stream_features
]
values = [
self.v_proj[scale_idx](feature).view(batch_size, self.num_heads, self.head_dim, height, width)
for feature in stream_features
]
if pad_h or pad_w:
query = F.pad(query, (0, pad_w, 0, pad_h))
keys = [F.pad(key, (0, pad_w, 0, pad_h)) for key in keys]
values = [F.pad(value, (0, pad_w, 0, pad_h)) for value in values]
key = torch.stack(keys, dim=1)
value = torch.stack(values, dim=1)
query_windows = self._window_partition_q(query, window_size)
key_windows = self._window_partition_kv(key, window_size)
value_windows = self._window_partition_kv(value, window_size)
attention = torch.matmul(query_windows, key_windows.transpose(-1, -2)) * self.scale
attention = F.softmax(attention, dim=-1)
context_windows = torch.matmul(attention, value_windows)
context = self._window_unpartition(context_windows, batch_size, padded_h, padded_w, window_size)
return context[:, :, :, :height, :width].reshape(batch_size, self.num_heads * self.head_dim, height, width)
def forward(
self,
stream_features: list[torch.Tensor],
unadapted: torch.Tensor,
scale_idx: int,
return_delta: bool = False,
evidence: Optional[torch.Tensor] = None,
) -> torch.Tensor:
if not stream_features:
delta = torch.zeros_like(unadapted)
adapted = unadapted
if return_delta:
return adapted, delta
return adapted
if scale_idx in self.global_spatial_scales:
context = self._global_spatial_context(stream_features, unadapted, scale_idx, evidence)
return self._apply_residual(context, unadapted, scale_idx, return_delta, evidence)
if scale_idx in self.windowed_spatial_scales:
context = self._windowed_spatial_context(stream_features, unadapted, scale_idx, evidence)
return self._apply_residual(context, unadapted, scale_idx, return_delta, evidence)
return super().forward(stream_features, unadapted, scale_idx, return_delta, evidence)
class Transformer2DBlock(nn.Module):
def __init__(
self,
hidden_dim: int,
num_heads: int = 4,
dropout_rate: float = 0.1,
window_size: Optional[int] = None,
mlp_ratio: int = 2,
) -> None:
super().__init__()
self.hidden_dim = hidden_dim
self.window_size = window_size
self.norm1 = nn.LayerNorm(hidden_dim)
self.attn = nn.MultiheadAttention(
hidden_dim,
num_heads=num_heads,
dropout=dropout_rate,
batch_first=True,
)
self.norm2 = nn.GroupNorm(_group_count(hidden_dim), hidden_dim)
expanded = hidden_dim * mlp_ratio
self.mlp = nn.Sequential(
nn.Conv2d(hidden_dim, expanded, kernel_size=1),
nn.GELU(),
nn.Conv2d(expanded, expanded, kernel_size=3, padding=1, groups=expanded),
nn.GELU(),
nn.Dropout2d(p=dropout_rate),
nn.Conv2d(expanded, hidden_dim, kernel_size=1),
nn.Dropout2d(p=dropout_rate),
)
def _global_attention(self, x: torch.Tensor) -> torch.Tensor:
batch_size, channels, height, width = x.shape
sequence = x.flatten(2).transpose(1, 2)
sequence_norm = self.norm1(sequence)
attention_out, _ = self.attn(sequence_norm, sequence_norm, sequence_norm, need_weights=False)
return attention_out.transpose(1, 2).reshape(batch_size, channels, height, width)
def _window_partition(self, x: torch.Tensor, window_size: int) -> torch.Tensor:
batch_size, channels, height, width = x.shape
return (
x.reshape(batch_size, channels, height // window_size, window_size, width // window_size, window_size)
.permute(0, 2, 4, 3, 5, 1)
.reshape(batch_size * (height // window_size) * (width // window_size), window_size * window_size, channels)
)
def _window_unpartition(
self,
windows: torch.Tensor,
batch_size: int,
height: int,
width: int,
window_size: int,
) -> torch.Tensor:
channels = windows.shape[-1]
return (
windows.reshape(
batch_size,
height // window_size,
width // window_size,
window_size,
window_size,
channels,
)
.permute(0, 5, 1, 3, 2, 4)
.reshape(batch_size, channels, height, width)
)
def _window_attention(self, x: torch.Tensor) -> torch.Tensor:
batch_size, _, height, width = x.shape
window_size = self.window_size
pad_h = (window_size - height % window_size) % window_size
pad_w = (window_size - width % window_size) % window_size
x_padded = F.pad(x, (0, pad_w, 0, pad_h)) if pad_h or pad_w else x
_, _, padded_h, padded_w = x_padded.shape
windows = self._window_partition(x_padded, window_size)
windows_norm = self.norm1(windows)
attention_windows, _ = self.attn(windows_norm, windows_norm, windows_norm, need_weights=False)
out = self._window_unpartition(attention_windows, batch_size, padded_h, padded_w, window_size)
return out[:, :, :height, :width]
def forward(self, x: torch.Tensor) -> torch.Tensor:
if self.window_size is None:
x = x + self._global_attention(x)
else:
x = x + self._window_attention(x)
x = x + self.mlp(self.norm2(x))
return x
class TransformerEvidenceMaskAdapter(nn.Module):
"""V2 mask adapter with evidence projection and efficient 2D transformer sharing."""
def __init__(
self,
hidden_dim: int = 128,
out_channels: int = 1,
output_resolution: tuple[int, int] = (128, 128),
in_channels_list: list[int] | None = None,
dropout_rate: float = 0.1,
num_heads: int = 4,
window_size: int = 8,
) -> None:
super().__init__()
channels = in_channels_list or [256, 32, 64]
self.output_resolution = output_resolution
self.hidden_dim = hidden_dim
self.mask_evidence_proj = nn.ModuleList(
[
FlexibleEvidenceProjector(
in_channels=hidden_dim + (2 * in_channels),
hidden_dim=hidden_dim,
dropout_rate=dropout_rate,
depth=2,
)
for in_channels in channels
]
)
self.low_blocks = nn.Sequential(
*[
Transformer2DBlock(hidden_dim, num_heads=num_heads, dropout_rate=dropout_rate, window_size=None)
for _ in range(2)
]
)
self.mid_fuse = FlexibleEvidenceProjector(
in_channels=hidden_dim * 2,
hidden_dim=hidden_dim,
dropout_rate=dropout_rate,
depth=1,
)
self.mid_blocks = nn.Sequential(
*[
Transformer2DBlock(hidden_dim, num_heads=num_heads, dropout_rate=dropout_rate, window_size=window_size)
for _ in range(2)
]
)
self.high_fuse = FlexibleEvidenceProjector(
in_channels=hidden_dim * 2,
hidden_dim=hidden_dim,
dropout_rate=dropout_rate,
depth=1,
)
self.high_blocks = nn.Sequential(
*[
Transformer2DBlock(hidden_dim, num_heads=num_heads, dropout_rate=dropout_rate, window_size=window_size)
for _ in range(2)
]
)
self.mask_head = nn.Sequential(
nn.GroupNorm(_group_count(hidden_dim), hidden_dim),
nn.Conv2d(hidden_dim, hidden_dim, kernel_size=3, padding=1),
nn.GELU(),
nn.Dropout2d(p=dropout_rate),
nn.Conv2d(hidden_dim, out_channels, kernel_size=1),
)
self.aux_head = nn.Sequential(
nn.GroupNorm(_group_count(hidden_dim), hidden_dim),
nn.Conv2d(hidden_dim, out_channels, kernel_size=1),
)
def _scale_evidence(
self,
adapted: FeaturePyramid,
unadapted: FeaturePyramid,
evidence: FeaturePyramid | None,
) -> FeaturePyramid:
projected = []
for scale_idx, projection in enumerate(self.mask_evidence_proj):
if evidence is None:
evidence_i = torch.zeros(
adapted[scale_idx].shape[0],
self.hidden_dim,
adapted[scale_idx].shape[-2],
adapted[scale_idx].shape[-1],
dtype=adapted[scale_idx].dtype,
device=adapted[scale_idx].device,
)
else:
evidence_i = evidence[scale_idx]
if evidence_i.shape[-2:] != adapted[scale_idx].shape[-2:]:
evidence_i = F.interpolate(
evidence_i,
size=adapted[scale_idx].shape[-2:],
mode="bilinear",
align_corners=False,
)
adapter_delta = adapted[scale_idx] - unadapted[scale_idx]
projected.append(
projection(torch.cat([evidence_i, adapted[scale_idx], adapter_delta.abs()], dim=1))
)
return projected
def forward(
self,
adapted: FeaturePyramid,
streams_unadapted: StreamPyramid,
unadapted: FeaturePyramid,
evidence: FeaturePyramid | None = None,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor | None]:
del streams_unadapted
high_target = self.output_resolution
scale_features = self._scale_evidence(adapted, unadapted, evidence)
low = self.low_blocks(scale_features[0])
mid = scale_features[2] + F.interpolate(
low,
size=scale_features[2].shape[-2:],
mode="bilinear",
align_corners=False,
)
mid = self.mid_fuse(torch.cat([mid, scale_features[2]], dim=1))
mid = self.mid_blocks(mid)
high = scale_features[1] + F.interpolate(
mid,
size=scale_features[1].shape[-2:],
mode="bilinear",
align_corners=False,
)
high = self.high_fuse(torch.cat([high, scale_features[1]], dim=1))
high = self.high_blocks(high)
if high.shape[-2:] != high_target:
high = F.interpolate(high, size=high_target, mode="bilinear", align_corners=False)
mask_logits = self.mask_head(high)
aux_logits = self.aux_head(mid)
return mask_logits, aux_logits, None