phanerozoic commited on
Commit
0ff2458
·
verified ·
1 Parent(s): fefb215

Add arena runner for NYU Depth V2 screening with RMSE eval

Browse files
Files changed (1) hide show
  1. arena.py +249 -0
arena.py ADDED
@@ -0,0 +1,249 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Depth Head Arena — train and evaluate heads on NYU Depth V2.
3
+
4
+ Usage:
5
+ python arena.py --head linear_probe --steps 2000 --batch 1
6
+ python arena.py --head all --steps 2000 --batch 1
7
+ python arena.py --list
8
+ """
9
+
10
+ import argparse
11
+ import json
12
+ import math
13
+ import os
14
+ import sys
15
+ import time
16
+
17
+ import numpy as np
18
+ import torch
19
+ import torch.nn.functional as F
20
+ from PIL import Image
21
+ from torch.utils.data import DataLoader, Dataset
22
+ from torchvision.transforms import v2
23
+
24
+ sys.path.insert(0, os.path.dirname(__file__))
25
+
26
+ BACKBONE_REPO = os.environ.get("ARENA_BACKBONE_REPO", "/home/zootest/EUPE")
27
+ BACKBONE_WEIGHTS = os.environ.get("ARENA_BACKBONE_WEIGHTS", "/home/zootest/weights/eupe_vitb/EUPE-ViT-B.pt")
28
+ BACKBONE_HUB_ENTRY = os.environ.get("ARENA_BACKBONE_ENTRY", "eupe_vitb16")
29
+ NYU_ROOT = os.environ.get("ARENA_NYU_ROOT", "/home/zootest/datasets/NYU")
30
+ CACHE_DIR = os.environ.get("ARENA_CACHE_DIR", "./arena_cache")
31
+ RESOLUTION = 416
32
+ MIN_DEPTH = 0.001
33
+ MAX_DEPTH = 10.0
34
+
35
+ if BACKBONE_REPO not in sys.path:
36
+ sys.path.insert(0, BACKBONE_REPO)
37
+
38
+ from heads import REGISTRY, ALL_NAMES, get_head
39
+
40
+
41
+ def cache_features(backbone, data_root, split, n_images, cache_path):
42
+ """Cache backbone features + depth maps."""
43
+ if os.path.isfile(cache_path):
44
+ print(f" Cache exists: {cache_path}", flush=True)
45
+ return
46
+
47
+ normalize = v2.Compose([
48
+ v2.ToImage(), v2.ToDtype(torch.float32, scale=True),
49
+ v2.Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)),
50
+ ])
51
+
52
+ # NYU structure: images in sync/ subdirectories
53
+ img_dir = os.path.join(data_root, split, "images")
54
+ depth_dir = os.path.join(data_root, split, "depth")
55
+
56
+ if not os.path.isdir(img_dir):
57
+ # Try flat structure
58
+ img_dir = os.path.join(data_root, split)
59
+ depth_dir = os.path.join(data_root, split)
60
+
61
+ fnames = sorted([f for f in os.listdir(img_dir) if f.endswith('.jpg') or f.endswith('.png')])[:n_images]
62
+ cached = []
63
+
64
+ print(f" Caching {len(fnames)} {split} images...", flush=True)
65
+ for i, fname in enumerate(fnames):
66
+ img = Image.open(os.path.join(img_dir, fname)).convert("RGB")
67
+ img_resized = img.resize((RESOLUTION, RESOLUTION), Image.BILINEAR)
68
+ x = normalize(img_resized).unsqueeze(0).cuda()
69
+
70
+ with torch.no_grad():
71
+ with torch.autocast("cuda", dtype=torch.bfloat16):
72
+ out = backbone.forward_features(x)
73
+ patches = out["x_norm_patchtokens"].float()
74
+ B, N, D = patches.shape
75
+ h = w = int(N ** 0.5)
76
+ spatial = patches[0].permute(1, 0).reshape(D, h, w).half().cpu()
77
+
78
+ # Load depth
79
+ depth_fname = fname.replace('.jpg', '.png')
80
+ depth_path = os.path.join(depth_dir, depth_fname)
81
+ if os.path.isfile(depth_path):
82
+ depth = np.array(Image.open(depth_path)).astype(np.float32) / 1000.0
83
+ depth = torch.from_numpy(depth).float()
84
+ depth = F.interpolate(depth.unsqueeze(0).unsqueeze(0),
85
+ size=(RESOLUTION, RESOLUTION), mode="bilinear",
86
+ align_corners=False)[0, 0]
87
+ else:
88
+ depth = torch.zeros(RESOLUTION, RESOLUTION)
89
+
90
+ cached.append({"spatial": spatial, "depth": depth})
91
+ if (i + 1) % 200 == 0:
92
+ print(f" {i+1}/{len(fnames)}", flush=True)
93
+
94
+ os.makedirs(os.path.dirname(cache_path), exist_ok=True)
95
+ torch.save(cached, cache_path)
96
+ print(f" Saved: {cache_path} ({len(cached)} images)", flush=True)
97
+
98
+
99
+ def compute_rmse(pred, gt, min_depth=MIN_DEPTH, max_depth=MAX_DEPTH):
100
+ mask = (gt > min_depth) & (gt < max_depth)
101
+ if mask.sum() == 0:
102
+ return float("nan")
103
+ return float(torch.sqrt(((pred[mask] - gt[mask]) ** 2).mean()))
104
+
105
+
106
+ def silog_loss(pred, target, variance_focus=0.85):
107
+ pred = pred.flatten()
108
+ target = target.flatten()
109
+ mask = target > MIN_DEPTH
110
+ pred = pred[mask].clamp(min=1e-6)
111
+ target = target[mask].clamp(min=1e-6)
112
+ if pred.numel() == 0:
113
+ return torch.tensor(0.0, device=pred.device)
114
+ d = torch.log(pred) - torch.log(target)
115
+ return torch.sqrt((d ** 2).mean() - variance_focus * (d.mean() ** 2) + 1e-8)
116
+
117
+
118
+ def run_candidate(head_name, train_data, val_data, steps=2000, lr=3e-4, seed=42):
119
+ torch.manual_seed(seed)
120
+ torch.cuda.manual_seed(seed)
121
+
122
+ head = get_head(head_name).cuda()
123
+ n_params = sum(p.numel() for p in head.parameters()) / 1e6
124
+ print(f"\n{'='*60}")
125
+ print(f" {head_name} ({n_params:.2f}M params)")
126
+ print(f"{'='*60}", flush=True)
127
+
128
+ optimizer = torch.optim.AdamW(head.parameters(), lr=lr, weight_decay=1e-3)
129
+ head.train()
130
+ n = len(train_data)
131
+ losses = []
132
+ t0 = time.time()
133
+
134
+ for step in range(steps):
135
+ idx = torch.randint(0, n, (1,)).item()
136
+ spatial = train_data[idx]["spatial"].unsqueeze(0).float().cuda()
137
+ depth_gt = train_data[idx]["depth"].unsqueeze(0).cuda()
138
+
139
+ pred = head(spatial)
140
+ pred_up = F.interpolate(pred, size=depth_gt.shape[1:], mode="bilinear", align_corners=False)
141
+ loss = silog_loss(pred_up.squeeze(1), depth_gt)
142
+
143
+ if torch.isnan(loss) or torch.isinf(loss):
144
+ optimizer.zero_grad()
145
+ continue
146
+
147
+ optimizer.zero_grad()
148
+ loss.backward()
149
+ torch.nn.utils.clip_grad_norm_(head.parameters(), 5.0)
150
+ optimizer.step()
151
+ losses.append(loss.item())
152
+
153
+ if (step + 1) % 500 == 0:
154
+ avg = np.mean(losses[-100:])
155
+ print(f" step {step+1}/{steps} loss={avg:.4f} ({time.time()-t0:.1f}s)", flush=True)
156
+
157
+ # Eval
158
+ head.eval()
159
+ rmses = []
160
+ with torch.no_grad():
161
+ for item in val_data:
162
+ spatial = item["spatial"].unsqueeze(0).float().cuda()
163
+ depth_gt = item["depth"]
164
+ pred = head(spatial)
165
+ pred_up = F.interpolate(pred, size=depth_gt.shape, mode="bilinear", align_corners=False)
166
+ pred_np = pred_up[0, 0].cpu()
167
+ rmse = compute_rmse(pred_np, depth_gt)
168
+ if not np.isnan(rmse):
169
+ rmses.append(rmse)
170
+
171
+ mean_rmse = float(np.mean(rmses)) if rmses else float("nan")
172
+
173
+ result = {
174
+ "name": head_name,
175
+ "params_M": n_params,
176
+ "loss_end": np.mean(losses[-10:]) if losses else float("nan"),
177
+ "rmse": mean_rmse,
178
+ "train_time_s": time.time() - t0,
179
+ }
180
+ print(f" loss: {result['loss_end']:.4f}, RMSE: {mean_rmse:.4f}", flush=True)
181
+ del head
182
+ torch.cuda.empty_cache()
183
+ return result
184
+
185
+
186
+ def main():
187
+ parser = argparse.ArgumentParser()
188
+ parser.add_argument("--head", default="all")
189
+ parser.add_argument("--steps", type=int, default=2000)
190
+ parser.add_argument("--list", action="store_true")
191
+ parser.add_argument("--n-train", type=int, default=500)
192
+ parser.add_argument("--n-val", type=int, default=100)
193
+ args = parser.parse_args()
194
+
195
+ if args.list:
196
+ for name in ALL_NAMES:
197
+ h = get_head(name)
198
+ p = sum(v.numel() for v in h.parameters()) / 1e6
199
+ print(f" {name:<25} {p:.2f}M")
200
+ return
201
+
202
+ print("=" * 60)
203
+ print("Depth Head Arena")
204
+ print("=" * 60, flush=True)
205
+
206
+ print("\nLoading backbone...", flush=True)
207
+ backbone = torch.hub.load(BACKBONE_REPO, BACKBONE_HUB_ENTRY, source="local", weights=BACKBONE_WEIGHTS)
208
+ backbone = backbone.cuda().eval()
209
+ for p in backbone.parameters():
210
+ p.requires_grad = False
211
+
212
+ print("\nCaching features...", flush=True)
213
+ train_cache = os.path.join(CACHE_DIR, "nyu_train.pt")
214
+ val_cache = os.path.join(CACHE_DIR, "nyu_val.pt")
215
+ cache_features(backbone, NYU_ROOT, "train", args.n_train, train_cache)
216
+ cache_features(backbone, NYU_ROOT, "test", args.n_val, val_cache)
217
+ del backbone
218
+ torch.cuda.empty_cache()
219
+
220
+ train_data = torch.load(train_cache, map_location="cpu", weights_only=False)
221
+ val_data = torch.load(val_cache, map_location="cpu", weights_only=False)
222
+ print(f" train: {len(train_data)}, val: {len(val_data)}", flush=True)
223
+
224
+ heads = ALL_NAMES if args.head == "all" else [h.strip() for h in args.head.split(",")]
225
+
226
+ results = []
227
+ for name in heads:
228
+ try:
229
+ r = run_candidate(name, train_data, val_data, steps=args.steps)
230
+ results.append(r)
231
+ except Exception as e:
232
+ print(f" ERROR: {name}: {e}", flush=True)
233
+ import traceback
234
+ traceback.print_exc()
235
+
236
+ print(f"\n{'='*60}")
237
+ print(f"{'Name':<25} {'Params':>7} {'RMSE':>8} {'Loss':>7}")
238
+ print("-" * 50)
239
+ for r in sorted(results, key=lambda x: x["rmse"] if not np.isnan(x["rmse"]) else 999):
240
+ print(f"{r['name']:<25} {r['params_M']:>6.2f}M {r['rmse']:>8.4f} {r['loss_end']:>7.3f}")
241
+
242
+ out = os.path.join(CACHE_DIR, "depth_results.json")
243
+ with open(out, "w") as f:
244
+ json.dump(results, f, indent=2)
245
+ print(f"\nSaved: {out}", flush=True)
246
+
247
+
248
+ if __name__ == "__main__":
249
+ main()