| """Robustness / generalization studies (§4.6). |
| |
| * pose noise : inject up to N degrees of rotation (+ optional translation) on the |
| non-reference context cameras and measure the reconstruction drop. MAGT's |
| map-anchored scaffold should degrade more gracefully than pixel-aligned methods. |
| * map noise : reuse :class:`mapgs.data.MapNoise` to build an imperfect map |
| (height noise / lane offsets / dropped segments) and re-evaluate — validates |
| the *weak-supervision* claim (no hard map dependence). |
| * cross-dataset: train on one dataset, evaluate (zero-shot / finetuned) on another |
| via the dataset adapters; map structure should transfer. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import math |
| from dataclasses import replace |
| from typing import Dict, List, Optional |
|
|
| import torch |
|
|
| from mapgs.eval.metrics import psnr, ssim |
| from mapgs.train.render import render_scene_views, extract_scene_dynamic |
|
|
|
|
| def _random_small_rotation(max_deg: float, device) -> torch.Tensor: |
| axis = torch.randn(3, device=device) |
| axis = axis / axis.norm().clamp_min(1e-8) |
| ang = math.radians(max_deg) * float(torch.empty(1).uniform_(-1, 1)) |
| x, y, z = axis |
| c, s, C = math.cos(ang), math.sin(ang), 1 - math.cos(ang) |
| return torch.tensor([ |
| [c + x * x * C, x * y * C - z * s, x * z * C + y * s], |
| [y * x * C + z * s, c + y * y * C, y * z * C - x * s], |
| [z * x * C - y * s, z * y * C + x * s, c + z * z * C], |
| ], device=device) |
|
|
|
|
| def perturb_context_poses(sample, rot_deg: float, trans: float = 0.0, device="cuda"): |
| """Return a copy of ``sample`` with noisy non-reference (idx>0) context poses.""" |
| c2w = sample.ctx_c2w.clone().to(device) |
| for v in range(1, c2w.shape[0]): |
| R = _random_small_rotation(rot_deg, device) |
| c2w[v, :3, :3] = R @ c2w[v, :3, :3] |
| if trans > 0: |
| c2w[v, :3, 3] = c2w[v, :3, 3] + trans * torch.randn(3, device=device) |
| return replace(sample, ctx_c2w=c2w.cpu()) |
|
|
|
|
| @torch.no_grad() |
| def pose_noise_sweep(evaluator, dataset, rot_degs: Optional[List[float]] = None, |
| trans: float = 0.0, max_scenes: int = 20) -> Dict[float, Dict[str, float]]: |
| rot_degs = rot_degs or [0.0, 2.0, 5.0, 10.0] |
| cfg = evaluator.cfg |
| H, W = cfg.data.height, cfg.data.width |
| out: Dict[float, Dict[str, float]] = {} |
| n = min(len(dataset), max_scenes) |
| for rd in rot_degs: |
| ps, ss = [], [] |
| for i in range(n): |
| s = dataset[i] |
| s = s if rd == 0.0 else perturb_context_poses(s, rd, trans, evaluator.device) |
| g, dyn = evaluator._decode(s) |
| rend = render_scene_views(evaluator.model, evaluator.ras, g, dyn, |
| s.sup_K.to(evaluator.device), s.sup_c2w.to(evaluator.device), |
| s.sup_frame.to(evaluator.device), H, W, evaluator.model.uses_features) |
| gt = s.sup_images.to(evaluator.device) |
| ps.append(float(psnr(rend["rgb"], gt))) |
| ss.append(float(ssim(rend["rgb"], gt))) |
| out[rd] = {"PSNR": sum(ps) / len(ps), "SSIM": sum(ss) / len(ss), "n": n} |
| return out |
|
|