Add arena runner for NYU Depth V2 screening with RMSE eval
Browse files
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()
|