models_animerun / aniunflow /segment_modules.py
doanh25032004's picture
Upload folder using huggingface_hub
872b0a0 verified
Raw
History Blame Contribute Delete
15.5 kB
# file: models/aniunflow/segment_modules.py
"""
Segment-Aware Model Extension Modules
======================================
Drop-in components for AniFlowFormer-T to enable segment-aware processing:
1. SegmentAwareCostModulation - Modulates cost volume based on segment affinity
2. SegmentAwareAttentionMask - Creates attention bias from segment structure
3. SegmentAwareRefinementHead - Refines flow using segment boundaries
All modules are independently toggleable for ablation studies.
"""
from __future__ import annotations
from typing import Dict, Optional, Tuple, List
import torch
import torch.nn as nn
import torch.nn.functional as F
from einops import rearrange
class SegmentAwareCostModulation(nn.Module):
"""
Segment-Aware Cost Modulation
==============================
Modulates the cost volume based on segment affinity between frames.
Pixels within the same segment across frames have their matching costs
boosted, while cross-segment matches are suppressed.
This helps flow estimation by leveraging segment structure as a prior.
"""
def __init__(
self,
cost_dim: int = 1,
segment_dim: int = 64,
modulation_strength: float = 0.5,
learnable: bool = True,
):
"""
Args:
cost_dim: Number of cost volume channels
segment_dim: Dimension for segment embedding projection
modulation_strength: Strength of modulation (0 = no effect, 1 = full)
learnable: Whether to use learnable projection layers
"""
super().__init__()
self.modulation_strength = modulation_strength
self.learnable = learnable
if learnable:
# Project segment features to modulation weights
self.segment_proj = nn.Sequential(
nn.Conv2d(segment_dim, segment_dim // 2, 1),
nn.ReLU(inplace=True),
nn.Conv2d(segment_dim // 2, 1, 1),
nn.Sigmoid(),
)
def forward(
self,
cost_volume: torch.Tensor, # (B, C, H, W) or (B, H1*W1, H2, W2)
segment_masks_1: torch.Tensor, # (B, S, H, W) source frame segments
segment_masks_2: torch.Tensor, # (B, S, H, W) target frame segments
) -> torch.Tensor:
"""
Modulate cost volume using segment affinity.
Args:
cost_volume: Cost volume to modulate
segment_masks_1: Segment masks for frame 1
segment_masks_2: Segment masks for frame 2
Returns:
Modulated cost volume (same shape as input)
"""
B = cost_volume.shape[0]
# Compute segment affinity matrix
# For each pixel pair, compute how likely they belong to same segment
affinity = self._compute_segment_affinity(segment_masks_1, segment_masks_2)
# Resize affinity to match cost volume spatial dimensions
if cost_volume.dim() == 4:
# Standard cost volume (B, C, H, W)
H, W = cost_volume.shape[-2:]
affinity = F.interpolate(
affinity.unsqueeze(1), size=(H, W),
mode='bilinear', align_corners=False
).squeeze(1) # (B, H, W)
# Apply modulation
modulation = 1.0 + self.modulation_strength * (affinity.unsqueeze(1) - 0.5)
cost_volume = cost_volume * modulation
return cost_volume
def _compute_segment_affinity(
self,
masks_1: torch.Tensor,
masks_2: torch.Tensor,
) -> torch.Tensor:
"""
Compute per-pixel segment affinity between two frames.
Returns affinity map where high values indicate pixels likely
belong to the same semantic segment.
"""
B, S, H, W = masks_1.shape
# Ensure same resolution
if masks_2.shape[-2:] != (H, W):
masks_2 = F.interpolate(
masks_2, size=(H, W), mode='bilinear', align_corners=False
)
# Compute per-pixel segment assignment (soft)
# For each pixel, we have a distribution over S segments
# Affinity = dot product of segment distributions
# Normalize to get probability distributions
masks_1_norm = masks_1 / (masks_1.sum(dim=1, keepdim=True).clamp(min=1e-6))
masks_2_norm = masks_2 / (masks_2.sum(dim=1, keepdim=True).clamp(min=1e-6))
# Element-wise product and sum over segments
affinity = (masks_1_norm * masks_2_norm).sum(dim=1) # (B, H, W)
return affinity
class SegmentAwareAttentionMask(nn.Module):
"""
Segment-Aware Attention Mask Generator
========================================
Creates attention bias from segment structure for transformer layers.
Tokens from the same segment attend more strongly to each other,
while cross-segment attention is relatively suppressed.
"""
def __init__(
self,
temperature: float = 1.0,
bias_scale: float = 2.0,
):
"""
Args:
temperature: Temperature for softmax (higher = softer attention)
bias_scale: Scale factor for attention bias
"""
super().__init__()
self.temperature = temperature
self.bias_scale = bias_scale
def forward(
self,
segment_masks: torch.Tensor, # (B, S, H, W)
query_shape: Tuple[int, int], # (h, w) for query tokens
key_shape: Optional[Tuple[int, int]] = None, # (h, w) for key tokens
) -> torch.Tensor:
"""
Generate attention bias from segment masks.
Args:
segment_masks: Segment masks (B, S, H, W)
query_shape: Spatial shape of query tokens
key_shape: Spatial shape of key tokens (default: same as query)
Returns:
attention_bias: (B, N_q, N_k) bias to add to attention logits
"""
B, S, H, W = segment_masks.shape
h_q, w_q = query_shape
h_k, w_k = key_shape if key_shape else query_shape
# Resize masks to query/key resolutions
masks_q = F.interpolate(
segment_masks, size=(h_q, w_q), mode='bilinear', align_corners=False
) # (B, S, h_q, w_q)
masks_k = F.interpolate(
segment_masks, size=(h_k, w_k), mode='bilinear', align_corners=False
) # (B, S, h_k, w_k)
# Flatten to token sequences
masks_q = rearrange(masks_q, 'b s h w -> b (h w) s') # (B, N_q, S)
masks_k = rearrange(masks_k, 'b s h w -> b s (h w)') # (B, S, N_k)
# Normalize to get segment probability
masks_q = masks_q / (masks_q.sum(dim=-1, keepdim=True).clamp(min=1e-6))
masks_k = masks_k / (masks_k.sum(dim=-1, keepdim=True).clamp(min=1e-6))
# Compute segment affinity as attention bias
# Tokens in same segment should have higher affinity
# affinity[i,j] = sum_s p(s|q_i) * p(s|k_j)
affinity = torch.bmm(masks_q, masks_k) # (B, N_q, N_k)
# Scale to attention bias range
attention_bias = self.bias_scale * (affinity - 0.5) / self.temperature
return attention_bias
class SegmentAwareRefinementHead(nn.Module):
"""
Segment-Aware Flow Refinement Head
====================================
Post-processing refinement that uses segment boundaries to improve
flow predictions, especially at object boundaries.
The refinement encourages:
- Smooth flow within segments
- Accurate flow discontinuities at segment boundaries
"""
def __init__(
self,
flow_dim: int = 2,
hidden_dim: int = 64,
num_layers: int = 2,
use_boundary_features: bool = True,
):
"""
Args:
flow_dim: Flow channels (typically 2)
hidden_dim: Hidden dimension for refinement network
num_layers: Number of refinement layers
use_boundary_features: Whether to use explicit boundary features
"""
super().__init__()
self.use_boundary_features = use_boundary_features
# Input: flow + optional boundary
in_dim = flow_dim + (1 if use_boundary_features else 0)
layers = []
for i in range(num_layers):
dim_in = in_dim if i == 0 else hidden_dim
dim_out = hidden_dim if i < num_layers - 1 else flow_dim
layers.extend([
nn.Conv2d(dim_in, dim_out, 3, padding=1),
nn.ReLU(inplace=True) if i < num_layers - 1 else nn.Identity(),
])
self.refine_net = nn.Sequential(*layers)
# Residual weight (learnable)
self.residual_weight = nn.Parameter(torch.tensor(0.1))
def forward(
self,
flow: torch.Tensor, # (B, 2, H, W)
boundary_map: torch.Tensor, # (B, 1, H, W)
) -> torch.Tensor:
"""
Refine flow using segment boundary information.
Args:
flow: Initial flow prediction
boundary_map: Segment boundary map (1 at boundaries)
Returns:
Refined flow (same shape as input)
"""
# Resize boundary if needed
if boundary_map.shape[-2:] != flow.shape[-2:]:
boundary_map = F.interpolate(
boundary_map, size=flow.shape[-2:],
mode='bilinear', align_corners=False
)
if self.use_boundary_features:
x = torch.cat([flow, boundary_map], dim=1)
else:
x = flow
# Compute residual refinement
residual = self.refine_net(x)
# Apply residual with learnable weight
refined_flow = flow + self.residual_weight * residual
return refined_flow
class SegmentGuidedCorrelation(nn.Module):
"""
Segment-Guided Correlation Layer
==================================
Computes correlation/cost volume with segment-aware weighting.
This replaces standard correlation with one that considers
segment structure when computing matching costs.
"""
def __init__(
self,
feature_dim: int,
segment_weight: float = 0.3,
radius: int = 4,
):
"""
Args:
feature_dim: Feature dimension
segment_weight: Weight for segment-based modulation
radius: Correlation search radius
"""
super().__init__()
self.segment_weight = segment_weight
self.radius = radius
# Segment feature projection
self.seg_proj = nn.Conv2d(feature_dim, feature_dim // 4, 1)
def forward(
self,
fmap1: torch.Tensor, # (B, C, H, W)
fmap2: torch.Tensor, # (B, C, H, W)
segment_masks_1: torch.Tensor, # (B, S, H, W)
segment_masks_2: torch.Tensor, # (B, S, H, W)
) -> torch.Tensor:
"""
Compute segment-guided correlation volume.
Args:
fmap1, fmap2: Feature maps from two frames
segment_masks_1, segment_masks_2: Corresponding segment masks
Returns:
corr: Correlation volume with segment modulation
"""
B, C, H, W = fmap1.shape
# Standard correlation (all pairs within radius)
# For simplicity, using global correlation here
fmap1_flat = fmap1.view(B, C, -1) # (B, C, HW)
fmap2_flat = fmap2.view(B, C, -1)
corr = torch.bmm(fmap1_flat.transpose(1, 2), fmap2_flat) # (B, HW, HW)
corr = corr / (C ** 0.5) # Scale by sqrt(d)
# Segment affinity modulation
# Resize masks to feature resolution
masks_1 = F.interpolate(segment_masks_1, size=(H, W), mode='bilinear', align_corners=False)
masks_2 = F.interpolate(segment_masks_2, size=(H, W), mode='bilinear', align_corners=False)
# Compute segment affinity
masks_1_flat = masks_1.view(B, -1, H * W) # (B, S, HW)
masks_2_flat = masks_2.view(B, -1, H * W)
# Normalize
masks_1_norm = masks_1_flat / (masks_1_flat.sum(dim=1, keepdim=True).clamp(min=1e-6))
masks_2_norm = masks_2_flat / (masks_2_flat.sum(dim=1, keepdim=True).clamp(min=1e-6))
# Segment affinity
seg_affinity = torch.bmm(masks_1_norm.transpose(1, 2), masks_2_norm) # (B, HW, HW)
# Modulate correlation
corr = corr + self.segment_weight * seg_affinity
# Reshape to spatial
corr = corr.view(B, H, W, H, W)
return corr
class SegmentAwareModuleBundle(nn.Module):
"""
Bundle of all segment-aware modules for easy integration.
This provides a unified interface to all segment-aware extensions,
with each component independently toggleable.
"""
def __init__(
self,
feature_dim: int = 128,
use_cost_modulation: bool = True,
use_attention_mask: bool = True,
use_refinement: bool = False,
):
super().__init__()
self.use_cost_modulation = use_cost_modulation
self.use_attention_mask = use_attention_mask
self.use_refinement = use_refinement
if use_cost_modulation:
self.cost_modulation = SegmentAwareCostModulation(
segment_dim=feature_dim
)
if use_attention_mask:
self.attention_mask = SegmentAwareAttentionMask()
if use_refinement:
self.refinement = SegmentAwareRefinementHead(
hidden_dim=feature_dim
)
def modulate_cost(
self,
cost_volume: torch.Tensor,
masks_1: torch.Tensor,
masks_2: torch.Tensor,
) -> torch.Tensor:
"""Apply cost modulation if enabled."""
if self.use_cost_modulation:
return self.cost_modulation(cost_volume, masks_1, masks_2)
return cost_volume
def get_attention_bias(
self,
segment_masks: torch.Tensor,
query_shape: Tuple[int, int],
key_shape: Optional[Tuple[int, int]] = None,
) -> Optional[torch.Tensor]:
"""Get attention bias if enabled."""
if self.use_attention_mask:
return self.attention_mask(segment_masks, query_shape, key_shape)
return None
def refine_flow(
self,
flow: torch.Tensor,
boundary_map: torch.Tensor,
) -> torch.Tensor:
"""Apply flow refinement if enabled."""
if self.use_refinement:
return self.refinement(flow, boundary_map)
return flow
# ============= Factory function =============
def build_segment_modules(cfg: Dict) -> SegmentAwareModuleBundle:
"""Build segment-aware module bundle from config."""
model_cfg = cfg.get('model', {}).get('args', {})
return SegmentAwareModuleBundle(
feature_dim=model_cfg.get('enc_channels', 64) * 2,
use_cost_modulation=model_cfg.get('use_segment_cost_modulation', False),
use_attention_mask=model_cfg.get('use_segment_attention_mask', False),
use_refinement=model_cfg.get('use_segment_refinement', False),
)