OdaxAI commited on
Commit
09f0673
·
verified ·
1 Parent(s): d5bddcd

Add scripts/extract_features.py

Browse files
Files changed (1) hide show
  1. scripts/extract_features.py +248 -0
scripts/extract_features.py ADDED
@@ -0,0 +1,248 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Feature extraction from frozen DeRIS backbone (Section 3.1-3.3 of Venice-H1).
4
+
5
+ Produces cached .pt files containing per-sample features:
6
+ - query_feat: [N_samples, N, D] query embeddings (D=256)
7
+ - det_scores: [N_samples, N] detection scores
8
+ - query_ious: [N_samples, N] per-query IoU vs GT
9
+ - oracle_idx: [N_samples] best query index
10
+ - mask_mean: [N_samples, N] μ_i = mean(P_i)
11
+ - mask_max: [N_samples, N] p̂_i = max(P_i)
12
+ - mask_area: [N_samples, N] a_i = mean(P_i > 0.5)
13
+ - mask_std: [N_samples, N] σ_i = std(P_i)
14
+ - grid_mean_4: [N_samples, N, 16] AvgPool 4×4
15
+ - grid_max_4: [N_samples, N, 16] MaxPool 4×4
16
+ - boundary_4: [N_samples, N] boundary energy at 4×4
17
+ - grid_mean_8: [N_samples, N, 64] AvgPool 8×8
18
+ - grid_max_8: [N_samples, N, 64] MaxPool 8×8
19
+ - boundary_8: [N_samples, N] boundary energy at 8×8
20
+ - grid_mean_16: [N_samples, N, 256] AvgPool 16×16
21
+ - grid_max_16: [N_samples, N, 256] MaxPool 16×16
22
+ - boundary_16: [N_samples, N] boundary energy at 16×16
23
+
24
+ Usage:
25
+ python scripts/extract_features.py \\
26
+ --deris_checkpoint /path/to/deris_l.pth \\
27
+ --data_root /path/to/refcoco/ \\
28
+ --dataset refcoco --split val \\
29
+ --output data/
30
+ """
31
+
32
+ import argparse
33
+ import os
34
+ from pathlib import Path
35
+
36
+ import torch
37
+ import torch.nn.functional as F
38
+ import numpy as np
39
+ from tqdm import tqdm
40
+
41
+
42
+ def compute_mask_statistics(mask_probs: torch.Tensor) -> dict:
43
+ """
44
+ Compute scalar mask statistics (Section 3.2, Eq. 1).
45
+
46
+ Args:
47
+ mask_probs: [N, H, W] sigmoid mask probabilities
48
+
49
+ Returns:
50
+ dict with mask_mean, mask_max, mask_area, mask_std (each [N])
51
+ """
52
+ N, H, W = mask_probs.shape
53
+ flat = mask_probs.reshape(N, -1)
54
+
55
+ return {
56
+ "mask_mean": flat.mean(dim=1), # μ_i
57
+ "mask_max": flat.max(dim=1).values, # p̂_i
58
+ "mask_area": (flat > 0.5).float().mean(1), # a_i
59
+ "mask_std": flat.std(dim=1), # σ_i
60
+ }
61
+
62
+
63
+ def compute_grid_signatures(mask_probs: torch.Tensor, grid_size: int) -> dict:
64
+ """
65
+ Compute multi-scale grid signatures (Section 3.3, Eqs. 2-4).
66
+
67
+ Args:
68
+ mask_probs: [N, H, W] mask probabilities
69
+ grid_size: G (one of 4, 8, 16)
70
+
71
+ Returns:
72
+ dict with grid_mean, grid_max, boundary (per query)
73
+ """
74
+ N = mask_probs.shape[0]
75
+ G = grid_size
76
+
77
+ # Reshape for pooling: [N, 1, H, W]
78
+ x = mask_probs.unsqueeze(1)
79
+
80
+ # Eq. 2: grid mean (AvgPool)
81
+ grid_mean = F.adaptive_avg_pool2d(x, (G, G)).reshape(N, G * G)
82
+
83
+ # Eq. 3: grid max (MaxPool)
84
+ grid_max = F.adaptive_max_pool2d(x, (G, G)).reshape(N, G * G)
85
+
86
+ # Eq. 4: boundary energy (mean absolute gradient of grid_mean)
87
+ grid_2d = grid_mean.reshape(N, G, G)
88
+ dx = (grid_2d[:, :, 1:] - grid_2d[:, :, :-1]).abs().mean(dim=(1, 2))
89
+ dy = (grid_2d[:, 1:, :] - grid_2d[:, :-1, :]).abs().mean(dim=(1, 2))
90
+ boundary = 0.5 * (dx + dy)
91
+
92
+ return {
93
+ f"grid_mean_{G}": grid_mean,
94
+ f"grid_max_{G}": grid_max,
95
+ f"boundary_{G}": boundary,
96
+ }
97
+
98
+
99
+ def compute_iou(pred_mask: torch.Tensor, gt_mask: torch.Tensor) -> float:
100
+ """Compute IoU between binary masks."""
101
+ pred = (pred_mask > 0.5).float()
102
+ gt = gt_mask.float()
103
+ intersection = (pred * gt).sum()
104
+ union = (pred + gt).clamp(0, 1).sum()
105
+ if union < 1:
106
+ return 0.0
107
+ return (intersection / union).item()
108
+
109
+
110
+ def extract_sample_features(
111
+ mask_logits: torch.Tensor,
112
+ query_embeddings: torch.Tensor,
113
+ det_scores: torch.Tensor,
114
+ gt_mask: torch.Tensor,
115
+ ) -> dict:
116
+ """
117
+ Extract all Venice-H1 features for one sample.
118
+
119
+ Args:
120
+ mask_logits: [N, H, W] raw mask logits from DeRIS
121
+ query_embeddings: [N, D] query embeddings
122
+ det_scores: [N] detection scores
123
+ gt_mask: [H_gt, W_gt] ground-truth binary mask
124
+
125
+ Returns:
126
+ dict with all features for this sample
127
+ """
128
+ N = mask_logits.shape[0]
129
+
130
+ # Eq. 1: mask probabilities
131
+ mask_probs = torch.sigmoid(mask_logits) # [N, H, W]
132
+
133
+ # Mask statistics (Section 3.2)
134
+ stats = compute_mask_statistics(mask_probs)
135
+
136
+ # Multi-scale grid signatures (Section 3.3)
137
+ grid_4 = compute_grid_signatures(mask_probs, 4)
138
+ grid_8 = compute_grid_signatures(mask_probs, 8)
139
+ grid_16 = compute_grid_signatures(mask_probs, 16)
140
+
141
+ # Compute IoU for each query vs GT
142
+ H_gt, W_gt = gt_mask.shape
143
+ mask_probs_resized = F.interpolate(
144
+ mask_probs.unsqueeze(1), size=(H_gt, W_gt),
145
+ mode='bilinear', align_corners=False
146
+ ).squeeze(1)
147
+
148
+ query_ious = torch.tensor([
149
+ compute_iou(mask_probs_resized[i], gt_mask) for i in range(N)
150
+ ])
151
+ oracle_idx = query_ious.argmax().item()
152
+
153
+ return {
154
+ "query_feat": query_embeddings, # [N, D]
155
+ "det_scores": det_scores, # [N]
156
+ "query_ious": query_ious, # [N]
157
+ "oracle_idx": oracle_idx,
158
+ **stats,
159
+ **grid_4,
160
+ **grid_8,
161
+ **grid_16,
162
+ }
163
+
164
+
165
+ def main():
166
+ parser = argparse.ArgumentParser(
167
+ description="Extract Venice-H1 features from frozen DeRIS.")
168
+ parser.add_argument("--deris_checkpoint", type=str, required=True,
169
+ help="Path to frozen DeRIS-L/B checkpoint")
170
+ parser.add_argument("--data_root", type=str, default="data/",
171
+ help="Root directory for RefCOCO data")
172
+ parser.add_argument("--dataset", type=str, required=True,
173
+ choices=["refcoco", "refcoco+", "refcocog"])
174
+ parser.add_argument("--split", type=str, required=True,
175
+ choices=["train", "val", "testA", "testB", "test"])
176
+ parser.add_argument("--output", type=str, default="data/",
177
+ help="Output directory for cached features")
178
+ parser.add_argument("--n_queries", type=int, default=10)
179
+ parser.add_argument("--batch_size", type=int, default=1)
180
+ parser.add_argument("--device", type=str, default="cuda")
181
+ args = parser.parse_args()
182
+
183
+ device = torch.device(args.device if torch.cuda.is_available() else "cpu")
184
+ os.makedirs(args.output, exist_ok=True)
185
+
186
+ print(f"Venice-H1 Feature Extraction")
187
+ print(f" Backbone: {args.deris_checkpoint}")
188
+ print(f" Dataset: {args.dataset} / {args.split}")
189
+ print(f" Device: {device}")
190
+ print()
191
+
192
+ # ---- Load DeRIS model ----
193
+ # NOTE: Adapt this import to your DeRIS installation path
194
+ # from deris.model import build_deris
195
+ # model = build_deris(args.deris_checkpoint).to(device).eval()
196
+ print("=" * 60)
197
+ print("IMPORTANT: You must adapt the model loading section below")
198
+ print("to your DeRIS installation. See comments in this script.")
199
+ print("=" * 60)
200
+ print()
201
+ print("Expected DeRIS outputs per sample:")
202
+ print(" - query_embeddings: [N, 256] (N=10 candidate queries)")
203
+ print(" - mask_logits: [N, H, W] (mask predictions)")
204
+ print(" - det_scores: [N] (detection confidence scores)")
205
+ print()
206
+ print("Once you have DeRIS producing these outputs, the feature")
207
+ print("extraction loop below handles everything else automatically.")
208
+ print()
209
+
210
+ # ---- Placeholder: replace with your data loader ----
211
+ # dataloader = build_refcoco_loader(args.data_root, args.dataset,
212
+ # args.split, batch_size=1)
213
+ #
214
+ # all_features = []
215
+ # for batch in tqdm(dataloader, desc=f"Extracting {args.split}"):
216
+ # img = batch["image"].to(device)
217
+ # expr = batch["expression"]
218
+ # gt_mask = batch["gt_mask"].to(device)
219
+ #
220
+ # with torch.no_grad():
221
+ # outputs = model(img, expr)
222
+ # mask_logits = outputs["pred_masks"][:args.n_queries]
223
+ # query_emb = outputs["query_embeddings"][:args.n_queries]
224
+ # scores = outputs["det_scores"][:args.n_queries]
225
+ #
226
+ # feats = extract_sample_features(
227
+ # mask_logits.squeeze(0), query_emb.squeeze(0),
228
+ # scores.squeeze(0), gt_mask.squeeze(0))
229
+ # all_features.append(feats)
230
+ #
231
+ # ---- Stack and save ----
232
+ # output_path = os.path.join(
233
+ # args.output,
234
+ # f"cached_{args.split}_{args.dataset}_unc_feats.pt")
235
+ # stacked = {k: torch.stack([f[k] for f in all_features])
236
+ # for k in all_features[0].keys()
237
+ # if k != "oracle_idx"}
238
+ # stacked["oracle_idx"] = torch.tensor(
239
+ # [f["oracle_idx"] for f in all_features])
240
+ # torch.save(stacked, output_path)
241
+ # print(f"Saved {len(all_features)} samples → {output_path}")
242
+
243
+ print("Feature extraction template ready.")
244
+ print("Uncomment the dataloader section above and adapt to your setup.")
245
+
246
+
247
+ if __name__ == "__main__":
248
+ main()