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
| """ GazeAnywhere: Promptable Gaze Target Estimation 论文: arXiv:2608.11367, CVPR 2026 核心: 冻结编码器特征融合 + Transformer 检测器 """
import torch import torch.nn as nn import torch.nn.functional as F from typing import Dict, Optional
class GazeAnywhereModel(nn.Module): """ GazeAnywhere 端到端视线目标估计模型 输入: 图像 + 文本/视觉提示 输出: 主体定位 + 帧内/帧外 + 视线热力图 """ def __init__(self, config: dict): super().__init__() self.visual_encoder = self._load_frozen_encoder(config['visual_encoder']) self.text_encoder = self._load_frozen_encoder(config['text_encoder']) self.fusion_transformer = nn.TransformerDecoder( d_model=config['d_model'], nhead=config['nhead'], num_layers=config['num_layers'], dim_feedforward=config['dim_ffn'], dropout=0.1 ) self.bbox_head = nn.Linear(config['d_model'], 4) self.presence_head = nn.Linear(config['d_model'], 2) self.gaze_head = nn.Sequential( nn.Linear(config['d_model'], config['d_model'] * 2), nn.GELU(), nn.Linear(config['d_model'] * 2, config['heatmap_size'] ** 2) ) self.heatmap_size = config['heatmap_size'] def _load_frozen_encoder(self, encoder_name: str): """加载冻结的预训练编码器""" encoder = nn.Identity() for param in encoder.parameters(): param.requires_grad = False return encoder def forward( self, image: torch.Tensor, text_prompt: Optional[str] = None, visual_prompt: Optional[torch.Tensor] = None ) -> Dict[str, torch.Tensor]: """ 前向传播 Args: image: 输入图像 (B, 3, H, W) text_prompt: 文本提示 (B, seq_len, d_model) visual_prompt: 视觉提示坐标 (B, 2) Returns: outputs: { 'bbox': 主体边界框 (B, 4), 'presence': 帧内/帧外概率 (B, 2), 'gaze_heatmap': 视线热力图 (B, H*W) } """ img_features = self.visual_encoder(image) if text_prompt is not None: prompt_features = self.text_encoder(text_prompt) else: prompt_features = img_features fused = self.fusion_transformer( img_features, prompt_features ) bbox = self.bbox_head(fused.mean(dim=1)) presence = self.presence_head(fused.mean(dim=1)) gaze_heatmap = self.gaze_head(fused.mean(dim=1)) gaze_heatmap = gaze_heatmap.view(-1, 1, self.heatmap_size, self.heatmap_size) return { 'bbox': bbox, 'presence': presence, 'gaze_heatmap': gaze_heatmap }
if __name__ == "__main__": config = { 'visual_encoder': 'clip-vit-base', 'text_encoder': 'clip-text-base', 'd_model': 768, 'nhead': 12, 'num_layers': 6, 'dim_ffn': 3072, 'heatmap_size': 64 } model = GazeAnywhereModel(config) image = torch.randn(2, 3, 224, 224) text_features = torch.randn(2, 10, 768) output = model(image, text_prompt=text_features) print(f"BoundingBox: {output['bbox'].shape}") print(f"Presence: {output['presence'].shape}") print(f"Gaze Heatmap: {output['gaze_heatmap'].shape}") print(f"Presence logits: {output['presence'][0]}")
|