1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199
| """ GazeAnywhere 模型架构
论文核心:transformer-based detector 融合冻结编码器特征 同时解决:主体定位 + 帧内外存在判断 + 视线目标热图估计 """
import torch import torch.nn as nn from typing import Dict, Optional, Tuple
class GazeAnywhere(nn.Module): """ GazeAnywhere: 可提示的视线目标估计 输入: - 图像 (B, 3, H, W) - 文本提示 (B, L) 或 视觉提示 (B, 2) 归一化坐标 输出: - 注视目标热图 (B, H//4, W//4) - 帧内外存在 (B, 1) - 主体定位 (B, 4) bbox """ def __init__( self, vision_model: str = 'CLIP-ViT-L/14', text_model: str = 'CLIP-text', embed_dim: int = 768, num_queries: int = 1, nhead: int = 8, num_decoder_layers: int = 6 ): super().__init__() self.vision_encoder = self._load_frozen_vision_encoder(vision_model) self.vision_proj = nn.Linear(1024, embed_dim) self.text_encoder = self._load_frozen_text_encoder(text_model) self.text_proj = nn.Linear(512, embed_dim) decoder_layer = nn.TransformerDecoderLayer( d_model=embed_dim, nhead=nhead, dim_feedforward=embed_dim * 4, batch_first=True, dropout=0.1 ) self.decoder = nn.TransformerDecoder( decoder_layer, num_layers=num_decoder_layers ) self.queries = nn.Parameter( torch.randn(num_queries, embed_dim) * 0.02 ) self.heatmap_head = nn.Sequential( nn.Linear(embed_dim, embed_dim // 2), nn.GELU(), nn.Linear(embed_dim // 2, (224 // 4) ** 2), nn.Sigmoid() ) self.presence_head = nn.Sequential( nn.Linear(embed_dim, embed_dim // 2), nn.GELU(), nn.Linear(embed_dim // 2, 1), nn.Sigmoid() ) self.bbox_head = nn.Sequential( nn.Linear(embed_dim, embed_dim // 2), nn.GELU(), nn.Linear(embed_dim // 2, 4), nn.Sigmoid() ) def _load_frozen_vision_encoder(self, name: str): """加载冻结的视觉编码器""" encoder = nn.TransformerEncoderLayer(d_model=1024, nhead=16) for p in encoder.parameters(): p.requires_grad = False return encoder def _load_frozen_text_encoder(self, name: str): """加载冻结的文本编码器""" encoder = nn.LSTM(512, 512, batch_first=True) for p in encoder.parameters(): p.requires_grad = False return encoder def forward( self, image: torch.Tensor, text_prompt: torch.Tensor = None, visual_prompt: torch.Tensor = None ) -> Dict[str, torch.Tensor]: """ Args: image: (B, 3, H, W) text_prompt: (B, L, 512) 文本特征 visual_prompt: (B, 2) 归一化坐标 [x, y] """ B = image.shape[0] visual_features = self.vision_proj( self._extract_patches(image) ) if text_prompt is not None: prompt_embed = self.text_proj(text_prompt) prompt_embed = prompt_embed.mean(dim=1, keepdim=True) elif visual_prompt is not None: prompt_embed = self._encode_visual_prompt( visual_prompt, visual_features ) else: raise ValueError("需要文本或视觉提示") queries = self.queries.unsqueeze(0).expand(B, -1, -1) keys_values = torch.cat([visual_features, prompt_embed], dim=1) decoded = self.decoder(queries, keys_values) heatmap = self.heatmap_head(decoded) heatmap = heatmap.view(B, -1, 56, 56) presence = self.presence_head(decoded) bbox = self.bbox_head(decoded) return { 'heatmap': heatmap, 'presence': presence, 'bbox': bbox, } def _extract_patches(self, image: torch.Tensor) -> torch.Tensor: """提取图像 patch 特征(简化版)""" B, C, H, W = image.shape patch_size = 16 patches = image.view( B, C, H // patch_size, patch_size, W // patch_size, patch_size ) patches = patches.permute(0, 2, 4, 1, 3, 5).contiguous() patches = patches.view(B, (H // patch_size) * (W // patch_size), -1) return patches def _encode_visual_prompt( self, prompt: torch.Tensor, features: torch.Tensor ) -> torch.Tensor: """视觉提示编码:归一化坐标 → 特征选择""" B = prompt.shape[0] N = features.shape[1] grid_size = int(N ** 0.5) x, y = prompt[:, 0], prompt[:, 1] idx = (y * grid_size + x).long() selected = features[torch.arange(B), idx] return selected.unsqueeze(1)
if __name__ == "__main__": torch.manual_seed(42) model = GazeAnywhere() image = torch.randn(2, 3, 224, 224) text = torch.randn(2, 10, 512) visual = torch.tensor([[0.5, 0.3], [0.2, 0.7]]) out_text = model(image, text_prompt=text) print("文本提示输出:") print(f" 热图: {out_text['heatmap'].shape}") print(f" 存在: {out_text['presence'].shape}") print(f" 定位: {out_text['bbox'].shape}") out_vis = model(image, visual_prompt=visual) print("\n视觉提示输出:") print(f" 热图: {out_vis['heatmap'].shape}") print(f" 存在: {out_vis['presence'].shape}") print(f" 定位: {out_vis['bbox'].shape}")
|