Upload folder using huggingface_hub
Browse files- tests/__init__.py +0 -0
- tests/conftest.py +47 -0
- tests/test_data_train_eval.py +83 -0
- tests/test_geometry.py +56 -0
- tests/test_hdmap.py +69 -0
- tests/test_losses.py +81 -0
- tests/test_model_render.py +82 -0
- tests/test_ttt.py +31 -0
- tests/test_unified.py +68 -0
tests/__init__.py
ADDED
|
File without changes
|
tests/conftest.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import tempfile
|
| 3 |
+
|
| 4 |
+
import pytest
|
| 5 |
+
import torch
|
| 6 |
+
|
| 7 |
+
from mapgs.config import load_config
|
| 8 |
+
|
| 9 |
+
CUDA = torch.cuda.is_available()
|
| 10 |
+
requires_cuda = pytest.mark.skipif(not CUDA, reason="needs CUDA (gsplat rasterizer)")
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def tiny_overrides(root):
|
| 14 |
+
return [
|
| 15 |
+
"model.embed_dim=128", "model.enc_depth=1", "model.dec_depth=2", "model.n_heads=4",
|
| 16 |
+
"model.tokens.n_map=128", "model.tokens.n_free=128",
|
| 17 |
+
"model.tokens.gaussians_per_token=4", "model.tokens.n_dyn_per_instance=16",
|
| 18 |
+
"model.feature_dim=8",
|
| 19 |
+
"data.height=48", "data.width=64", "data.num_frames=8", "data.synth_dynamic_actors=1",
|
| 20 |
+
f"data.root={root}",
|
| 21 |
+
"train.batch_size=2", "train.amp=false", "train.num_workers=0",
|
| 22 |
+
"train.warmup=2", "train.extrap_ramp_iter=2", "train.log_every=1000", "train.ckpt_every=0",
|
| 23 |
+
"loss.lambda_lpips=0.0", "tt.steps=3",
|
| 24 |
+
]
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
@pytest.fixture(scope="session")
|
| 28 |
+
def device():
|
| 29 |
+
return "cuda" if CUDA else "cpu"
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
@pytest.fixture(scope="session")
|
| 33 |
+
def data_root(tmp_path_factory):
|
| 34 |
+
return str(tmp_path_factory.mktemp("synthetic"))
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
@pytest.fixture(scope="session")
|
| 38 |
+
def cfg(data_root):
|
| 39 |
+
return load_config(overrides=tiny_overrides(data_root))
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
@pytest.fixture(scope="session")
|
| 43 |
+
def dataset(cfg):
|
| 44 |
+
if not CUDA:
|
| 45 |
+
pytest.skip("synthetic GT rendering needs CUDA")
|
| 46 |
+
from mapgs.data import SyntheticDataset
|
| 47 |
+
return SyntheticDataset(cfg, "train", n_scenes=4, n_sup_views=4, device="cuda")
|
tests/test_data_train_eval.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
|
| 3 |
+
from mapgs.data import collate_samples
|
| 4 |
+
from mapgs.train import Trainer, compute_step_losses
|
| 5 |
+
from mapgs.train.trainer import _move_batch
|
| 6 |
+
from mapgs.eval import Evaluator, psnr, d_rmse
|
| 7 |
+
from .conftest import requires_cuda
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
@requires_cuda
|
| 11 |
+
def test_sample_shapes(cfg, dataset):
|
| 12 |
+
from mapgs.data.synthetic import _context_frames
|
| 13 |
+
s = dataset[0]
|
| 14 |
+
Vc = len(_context_frames(cfg)) * 3 # context frames clamp+dedup within num_frames
|
| 15 |
+
assert s.ctx_images.shape == (Vc, 3, cfg.data.height, cfg.data.width)
|
| 16 |
+
assert s.ctx_tids.shape[0] == Vc
|
| 17 |
+
assert s.anchor_pos.shape == (cfg.model.tokens.n_map, 3)
|
| 18 |
+
assert s.sup_depth.shape[0] == s.sup_images.shape[0]
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
@requires_cuda
|
| 22 |
+
def test_collate_and_dynamic_padding(cfg, dataset):
|
| 23 |
+
batch = collate_samples([dataset[0], dataset[1]])
|
| 24 |
+
assert batch["ctx_images"].shape[0] == 2
|
| 25 |
+
if batch["dynamic"] is not None:
|
| 26 |
+
assert batch["dynamic"]["box_centers"].shape[0] == 2
|
| 27 |
+
assert batch["dynamic"]["valid"].shape[0] == 2
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
@requires_cuda
|
| 31 |
+
def test_step_losses_finite_and_backward(cfg, dataset):
|
| 32 |
+
trainer = Trainer(cfg)
|
| 33 |
+
batch = _move_batch(collate_samples([dataset[0], dataset[1]]), "cuda")
|
| 34 |
+
total, log = compute_step_losses(trainer.model, trainer.ras, batch, trainer.criterion, 100, cfg, "cuda")
|
| 35 |
+
assert torch.isfinite(total)
|
| 36 |
+
for k in ["rgb", "mapdepth", "vis", "vert", "free_space"]:
|
| 37 |
+
assert k in log
|
| 38 |
+
total.backward()
|
| 39 |
+
assert sum(p.grad.abs().sum() for p in trainer.model.parameters() if p.grad is not None) > 0
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
@requires_cuda
|
| 43 |
+
def test_amp_train_step_runs(cfg, dataset):
|
| 44 |
+
# guards the autocast (bf16) -> fp32 rendering boundary
|
| 45 |
+
prev = cfg.train.amp
|
| 46 |
+
cfg.train.amp = True
|
| 47 |
+
try:
|
| 48 |
+
trainer = Trainer(cfg)
|
| 49 |
+
log = trainer.train_step(collate_samples([dataset[0], dataset[1]]))
|
| 50 |
+
assert log["total"] == log["total"] # finite (not NaN)
|
| 51 |
+
assert log["grad_norm"] >= 0
|
| 52 |
+
finally:
|
| 53 |
+
cfg.train.amp = prev
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
@requires_cuda
|
| 57 |
+
def test_training_decreases_loss(cfg, dataset):
|
| 58 |
+
trainer = Trainer(cfg)
|
| 59 |
+
losses = []
|
| 60 |
+
for _ in range(20):
|
| 61 |
+
batch = collate_samples([dataset[0], dataset[1]])
|
| 62 |
+
losses.append(trainer.train_step(batch)["total"])
|
| 63 |
+
assert min(losses[10:]) < losses[0] # improved over the first step
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
@requires_cuda
|
| 67 |
+
def test_metrics_sanity():
|
| 68 |
+
img = torch.rand(3, 16, 16, device="cuda")
|
| 69 |
+
assert psnr(img, img) > 50
|
| 70 |
+
d = torch.rand(1, 8, 8, device="cuda") + 1
|
| 71 |
+
assert d_rmse(d, d.clone()) < 1e-5
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
@requires_cuda
|
| 75 |
+
def test_evaluator_runs(cfg, dataset):
|
| 76 |
+
trainer = Trainer(cfg)
|
| 77 |
+
ev = Evaluator(trainer.model, cfg, device="cuda")
|
| 78 |
+
interp = ev.interpolation(dataset, max_scenes=2)
|
| 79 |
+
assert "PSNR" in interp and "D-RMSE" in interp
|
| 80 |
+
sweep = ev.extrapolation_sweep(dataset, shifts=[2.0], max_scenes=2, frame=cfg.data.num_frames // 2)
|
| 81 |
+
assert 2.0 in sweep
|
| 82 |
+
lane = ev.lane_consistency(dataset, max_scenes=2, frame=cfg.data.num_frames // 2)
|
| 83 |
+
assert "lane_mIoU" in lane
|
tests/test_geometry.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import math
|
| 2 |
+
|
| 3 |
+
import torch
|
| 4 |
+
|
| 5 |
+
from mapgs.geometry import (
|
| 6 |
+
quat_to_rotmat, rotmat_to_quat, se3_inverse, project_points,
|
| 7 |
+
backproject_depth, plucker_embedding, look_at_pose,
|
| 8 |
+
)
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def test_quat_rotmat_roundtrip():
|
| 12 |
+
q = torch.randn(20, 4)
|
| 13 |
+
q = q / q.norm(dim=-1, keepdim=True)
|
| 14 |
+
R = quat_to_rotmat(q)
|
| 15 |
+
# orthonormal, det 1
|
| 16 |
+
eye = R @ R.transpose(-1, -2)
|
| 17 |
+
assert torch.allclose(eye, torch.eye(3).expand_as(eye), atol=1e-5)
|
| 18 |
+
assert torch.allclose(torch.det(R), torch.ones(20), atol=1e-5)
|
| 19 |
+
q2 = rotmat_to_quat(R)
|
| 20 |
+
R2 = quat_to_rotmat(q2)
|
| 21 |
+
assert torch.allclose(R, R2, atol=1e-5)
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def test_se3_inverse():
|
| 25 |
+
T = torch.eye(4).repeat(5, 1, 1)
|
| 26 |
+
T[:, :3, :3] = quat_to_rotmat(torch.randn(5, 4))
|
| 27 |
+
T[:, :3, 3] = torch.randn(5, 3)
|
| 28 |
+
I = T @ se3_inverse(T)
|
| 29 |
+
assert torch.allclose(I, torch.eye(4).expand_as(I), atol=1e-5)
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def test_projection_roundtrip():
|
| 33 |
+
K = torch.tensor([[300., 0, 224], [0, 300, 128], [0, 0, 1]])[None]
|
| 34 |
+
c2w = look_at_pose(torch.tensor([0., 0, 1.5]), torch.tensor([0., 20, 0.5]))[None]
|
| 35 |
+
pts = torch.randn(1, 100, 3)
|
| 36 |
+
pts[..., 1] = pts[..., 1].abs() + 5 # in front
|
| 37 |
+
uv, z = project_points(pts, K, se3_inverse(c2w))
|
| 38 |
+
back = backproject_depth(uv, z, K, c2w)
|
| 39 |
+
assert torch.allclose(back, pts, atol=1e-3)
|
| 40 |
+
assert (z > 0).all()
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def test_look_at_forward_is_positive_depth():
|
| 44 |
+
# a point in front of a forward-looking camera must have positive cam-z
|
| 45 |
+
c2w = look_at_pose(torch.tensor([0., 0, 1.5]), torch.tensor([0., 30, 0.5]))[None]
|
| 46 |
+
K = torch.tensor([[300., 0, 224], [0, 300, 128], [0, 0, 1]])[None]
|
| 47 |
+
pt = torch.tensor([[[0., 10, 0.5]]])
|
| 48 |
+
_, z = project_points(pt, K, se3_inverse(c2w))
|
| 49 |
+
assert z.item() > 0
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def test_plucker_shape():
|
| 53 |
+
K = torch.tensor([[300., 0, 224], [0, 300, 128], [0, 0, 1]])[None].repeat(3, 1, 1)
|
| 54 |
+
c2w = torch.eye(4)[None].repeat(3, 1, 1)
|
| 55 |
+
pl = plucker_embedding(K, c2w, 32, 48)
|
| 56 |
+
assert pl.shape == (3, 6, 32, 48)
|
tests/test_hdmap.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import torch
|
| 3 |
+
|
| 4 |
+
from mapgs.config import MapConfig
|
| 5 |
+
from mapgs.hdmap import (
|
| 6 |
+
grid_field_from_points, HDMap, sample_anchors, rasterize_map_depth, project_polylines,
|
| 7 |
+
)
|
| 8 |
+
from mapgs.hdmap.anchors import resample_polyline, weighted_fps
|
| 9 |
+
from .conftest import requires_cuda
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def _toy_map():
|
| 13 |
+
pts = np.random.RandomState(0).uniform([-20, -5], [20, 50], size=(2000, 2))
|
| 14 |
+
z = 0.02 * pts[:, 0]
|
| 15 |
+
gp = np.concatenate([pts, z[:, None]], 1)
|
| 16 |
+
gf = grid_field_from_points(gp, 0.5)
|
| 17 |
+
lanes = [torch.tensor([[x, y, 0.02 * x] for y in np.arange(0, 45, 1.0)], dtype=torch.float32)
|
| 18 |
+
for x in [-3.5, 0, 3.5]]
|
| 19 |
+
bnds = [torch.tensor([[x, y, 0.02 * x] for y in np.arange(0, 45, 2.0)], dtype=torch.float32)
|
| 20 |
+
for x in [-7, 7]]
|
| 21 |
+
return HDMap(ground=gf, lanes=lanes, boundaries=bnds)
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def test_ground_field_bilinear_and_grad():
|
| 25 |
+
hd = _toy_map()
|
| 26 |
+
xy = torch.tensor([[1.0, 2.0], [3.0, 4.0]], requires_grad=True)
|
| 27 |
+
z, valid = hd.height_at(xy)
|
| 28 |
+
assert valid.all()
|
| 29 |
+
assert torch.allclose(z, 0.02 * xy[:, 0].detach(), atol=0.05)
|
| 30 |
+
z.sum().backward()
|
| 31 |
+
assert xy.grad.abs().sum() > 0
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def test_anchor_ratios_and_count():
|
| 35 |
+
hd = _toy_map()
|
| 36 |
+
cfg = MapConfig(n_anchors=1000)
|
| 37 |
+
A = sample_anchors(hd, cfg, seed=0)
|
| 38 |
+
assert len(A) == 1000
|
| 39 |
+
counts = torch.bincount(A.types, minlength=3).float() / 1000
|
| 40 |
+
assert abs(counts[0] - 0.6) < 0.05
|
| 41 |
+
assert abs(counts[1] - 0.3) < 0.05
|
| 42 |
+
assert abs(counts[2] - 0.1) < 0.05
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def test_resample_polyline_spacing():
|
| 46 |
+
pl = torch.tensor([[0., 0, 0], [0, 10, 0]])
|
| 47 |
+
out = resample_polyline(pl, 1.0)
|
| 48 |
+
assert out.shape[0] >= 9
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def test_weighted_fps_prefers_high_weight():
|
| 52 |
+
pts = torch.randn(200, 3)
|
| 53 |
+
w = torch.ones(200)
|
| 54 |
+
w[0] = 100.0
|
| 55 |
+
sel = weighted_fps(pts, w, 10)
|
| 56 |
+
assert 0 in sel.tolist() # highest-weight point is picked first
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
@requires_cuda
|
| 60 |
+
def test_map_depth_coverage():
|
| 61 |
+
from mapgs.geometry import look_at_pose
|
| 62 |
+
hd = _toy_map().to("cuda")
|
| 63 |
+
K = torch.tensor([[60., 0, 32], [0, 60, 24], [0, 0, 1]], device="cuda")[None]
|
| 64 |
+
c2w = look_at_pose(torch.tensor([0., 0, 1.5]), torch.tensor([0., 20, 0.5]))[None].cuda()
|
| 65 |
+
depth, mask = rasterize_map_depth(hd.ground, K, c2w, 48, 64)
|
| 66 |
+
assert mask.float().mean() > 0.2 # lower half sees ground
|
| 67 |
+
assert (depth[mask] > 0).all()
|
| 68 |
+
uv = project_polylines(hd.lanes, K, c2w, 48, 64)
|
| 69 |
+
assert uv[0].shape[0] > 0
|
tests/test_losses.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
|
| 3 |
+
from mapgs.config import LossConfig, TokenConfig
|
| 4 |
+
from mapgs.losses import (
|
| 5 |
+
Tempering, ssim, chamfer_2d, mapdepth_loss, free_space_loss,
|
| 6 |
+
ground_coupling_loss, silog_loss, visibility_loss, huber,
|
| 7 |
+
)
|
| 8 |
+
from mapgs.losses.extrap import perturb_pose
|
| 9 |
+
from mapgs.geometry import look_at_pose
|
| 10 |
+
from mapgs.hdmap import grid_field_from_points
|
| 11 |
+
from mapgs.render.gaussians import Gaussians, GROUP_DYNAMIC
|
| 12 |
+
import numpy as np
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def test_tempering_monotone_and_capped():
|
| 16 |
+
t = Tempering(LossConfig(), TokenConfig(), total_iters=150000)
|
| 17 |
+
assert t.eps(0) < t.eps(1000) <= t.eps_max
|
| 18 |
+
assert t.s(0) < t.s(1000) <= t.s_max
|
| 19 |
+
assert t.eps(10_000_000) == t.eps_max # capped
|
| 20 |
+
assert t.lambda_md_scale(0) == 1.0
|
| 21 |
+
assert t.lambda_md_scale(149000) == LossConfig().md_late_decay
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def test_ssim_identical_is_one():
|
| 25 |
+
img = torch.rand(1, 3, 32, 32)
|
| 26 |
+
assert ssim(img, img) > 0.999
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def test_mapdepth_zero_when_equal():
|
| 30 |
+
d = torch.rand(2, 16, 16) + 1
|
| 31 |
+
mask = torch.ones(2, 16, 16, dtype=torch.bool)
|
| 32 |
+
assert mapdepth_loss(d, d.clone(), mask, eps=0.3) < 1e-6
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def test_huber_matches_quadratic_for_small():
|
| 36 |
+
x = torch.tensor([0.1, -0.1])
|
| 37 |
+
assert torch.allclose(huber(x, 1.0), 0.5 * x ** 2)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def test_visibility_inside_vs_outside():
|
| 41 |
+
K = torch.tensor([[60., 0, 32], [0, 60, 24], [0, 0, 1]])[None]
|
| 42 |
+
c2w = look_at_pose(torch.tensor([0., 0, 1.5]), torch.tensor([0., 20, 0.5]))[None]
|
| 43 |
+
inside = torch.tensor([[0., 12, 0.5]]) # in front, centered
|
| 44 |
+
outside = torch.tensor([[0., -30, 0.5]]) # behind camera
|
| 45 |
+
assert visibility_loss(inside, K, c2w, 48, 64) < 1e-4
|
| 46 |
+
assert visibility_loss(outside, K, c2w, 48, 64) > 0
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def test_free_space_and_ground_coupling():
|
| 50 |
+
gp = np.concatenate([np.random.RandomState(0).uniform([-10, -5], [10, 20], (500, 2)),
|
| 51 |
+
np.zeros((500, 1))], 1)
|
| 52 |
+
gf = grid_field_from_points(gp, 0.5)
|
| 53 |
+
n = 50
|
| 54 |
+
means = torch.zeros(n, 3); means[:, 2] = -1.0 # all below ground
|
| 55 |
+
g = Gaussians(means, torch.full((n, 3), 0.1), _identq(n), torch.full((n,), 0.8), torch.rand(n, 3),
|
| 56 |
+
group=torch.full((n,), GROUP_DYNAMIC))
|
| 57 |
+
assert free_space_loss(g, gf, delta=0.1) > 0 # penalizes below-ground opacity
|
| 58 |
+
assert ground_coupling_loss(g, gf, eps=1.0) > 0 # pulls dynamic z up
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def test_silog_zero_when_equal():
|
| 62 |
+
d = torch.rand(1, 16, 16) + 1
|
| 63 |
+
mask = torch.ones(1, 16, 16, dtype=torch.bool)
|
| 64 |
+
assert abs(float(silog_loss(d, d.clone(), mask))) < 1e-6
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def test_chamfer_zero_for_identical():
|
| 68 |
+
a = torch.randn(20, 2)
|
| 69 |
+
assert chamfer_2d(a, a.clone()) < 1e-6
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def test_perturb_pose_shifts_laterally():
|
| 73 |
+
c2w = look_at_pose(torch.tensor([0., 0, 1.5]), torch.tensor([0., 20, 0.5]))
|
| 74 |
+
dev = perturb_pose(c2w, lateral=2.0)
|
| 75 |
+
shift = (dev[:3, 3] - c2w[:3, 3]).norm()
|
| 76 |
+
assert abs(float(shift) - 2.0) < 1e-4
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def _identq(n):
|
| 80 |
+
q = torch.zeros(n, 4); q[:, 0] = 1
|
| 81 |
+
return q
|
tests/test_model_render.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
|
| 3 |
+
from mapgs.model import MapGS
|
| 4 |
+
from mapgs.render import Gaussians, GaussianRasterizer, GROUP_MAP, GROUP_DYNAMIC
|
| 5 |
+
from mapgs.geometry import plucker_embedding, look_at_pose
|
| 6 |
+
from .conftest import requires_cuda
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def _model_inputs(cfg, device, B=1, dynamic=False):
|
| 10 |
+
V, H, W = 3, cfg.data.height, cfg.data.width
|
| 11 |
+
imgs = torch.rand(B, V, 3, H, W, device=device)
|
| 12 |
+
K = torch.tensor([[60., 0, W / 2], [0, 60, H / 2], [0, 0, 1]], device=device)[None, None].repeat(B, V, 1, 1)
|
| 13 |
+
c2w = torch.stack([look_at_pose(torch.tensor([dx, 0., 1.5]), torch.tensor([dx, 20., 0.5]))
|
| 14 |
+
for dx in [-1., 0, 1]]).to(device)[None].repeat(B, 1, 1, 1)
|
| 15 |
+
pl = torch.stack([plucker_embedding(K[b], c2w[b], H, W) for b in range(B)])
|
| 16 |
+
tids = torch.zeros(B, V, dtype=torch.long, device=device)
|
| 17 |
+
nmap = cfg.model.tokens.n_map
|
| 18 |
+
apos = torch.randn(B, nmap, 3, device=device); apos[..., 2] *= 0.1
|
| 19 |
+
atype = torch.randint(0, 3, (B, nmap), device=device)
|
| 20 |
+
anorm = torch.zeros(B, nmap, 3, device=device); anorm[..., 2] = 1
|
| 21 |
+
dyn = None
|
| 22 |
+
if dynamic:
|
| 23 |
+
I, F = 2, cfg.data.num_frames
|
| 24 |
+
centers = torch.zeros(B, I, F, 3, device=device)
|
| 25 |
+
for f in range(F):
|
| 26 |
+
centers[:, 0, f] = torch.tensor([3., 5 + 0.5 * f, 0.5], device=device)
|
| 27 |
+
centers[:, 1, f] = torch.tensor([-3., 8., 0.5], device=device)
|
| 28 |
+
dyn = dict(box_centers=centers, box_rots=torch.eye(3, device=device).view(1, 1, 1, 3, 3).repeat(B, I, F, 1, 1),
|
| 29 |
+
box_size=torch.ones(B, I, 3, device=device) * 2, valid=torch.ones(B, I, dtype=torch.bool, device=device),
|
| 30 |
+
canon_idx=torch.zeros(B, I, dtype=torch.long, device=device))
|
| 31 |
+
return imgs, pl, tids, apos, atype, anorm, K, c2w, dyn
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
@requires_cuda
|
| 35 |
+
def test_forward_shapes_and_budget(cfg):
|
| 36 |
+
model = MapGS(cfg).cuda()
|
| 37 |
+
imgs, pl, tids, apos, atype, anorm, K, c2w, _ = _model_inputs(cfg, "cuda")
|
| 38 |
+
g = model(imgs, pl, tids, apos, atype, anorm, s_t=0.5)
|
| 39 |
+
M = (cfg.model.tokens.n_map + cfg.model.tokens.n_free) * cfg.model.tokens.gaussians_per_token
|
| 40 |
+
assert g.means.shape == (1, M, 3)
|
| 41 |
+
assert g.colors.shape[-1] == cfg.model.feature_dim
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
@requires_cuda
|
| 45 |
+
def test_bounded_residual_for_map_tokens(cfg):
|
| 46 |
+
model = MapGS(cfg).cuda()
|
| 47 |
+
imgs, pl, tids, apos, atype, anorm, K, c2w, _ = _model_inputs(cfg, "cuda")
|
| 48 |
+
s_t = 0.5
|
| 49 |
+
g = model(imgs, pl, tids, apos, atype, anorm, s_t=s_t)
|
| 50 |
+
is_map = g.group[0] == GROUP_MAP
|
| 51 |
+
# map gaussian means must lie within s_t of their anchor (sqrt(3)*s_t bound on the cube)
|
| 52 |
+
means = g.means[0][is_map]
|
| 53 |
+
ng = cfg.model.tokens.gaussians_per_token
|
| 54 |
+
anchors = apos[0].repeat_interleave(ng, 0)[: means.shape[0]]
|
| 55 |
+
dev = (means - anchors).abs().max()
|
| 56 |
+
assert dev <= s_t + 1e-4
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
@requires_cuda
|
| 60 |
+
def test_dynamic_placement_moves_only_dynamic(cfg):
|
| 61 |
+
model = MapGS(cfg).cuda()
|
| 62 |
+
imgs, pl, tids, apos, atype, anorm, K, c2w, dyn = _model_inputs(cfg, "cuda", dynamic=True)
|
| 63 |
+
g = model(imgs, pl, tids, apos, atype, anorm, s_t=0.5, dynamic=dyn)
|
| 64 |
+
g0 = model.place_dynamics(g, dyn, 0)
|
| 65 |
+
g1 = model.place_dynamics(g, dyn, cfg.data.num_frames - 1)
|
| 66 |
+
dynm = g.group[0] == GROUP_DYNAMIC
|
| 67 |
+
assert (g1.means[0][dynm] - g0.means[0][dynm]).norm(dim=-1).mean() > 0.1
|
| 68 |
+
assert torch.allclose(g1.means[0][~dynm], g0.means[0][~dynm])
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
@requires_cuda
|
| 72 |
+
def test_render_and_backward(cfg):
|
| 73 |
+
model = MapGS(cfg).cuda()
|
| 74 |
+
imgs, pl, tids, apos, atype, anorm, K, c2w, _ = _model_inputs(cfg, "cuda")
|
| 75 |
+
g = model(imgs, pl, tids, apos, atype, anorm, s_t=0.5)
|
| 76 |
+
ras = GaussianRasterizer()
|
| 77 |
+
out = ras.render(g.scene(0), K[0], c2w[0], cfg.data.height, cfg.data.width)
|
| 78 |
+
rgb = model.feature_to_rgb(out.color)
|
| 79 |
+
assert rgb.shape == (3, 3, cfg.data.height, cfg.data.width)
|
| 80 |
+
assert out.aux is not None # lane channel rendered
|
| 81 |
+
(rgb.mean() + out.depth.mean()).backward()
|
| 82 |
+
assert sum(p.grad.abs().sum() for p in model.parameters() if p.grad is not None) > 0
|
tests/test_ttt.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
|
| 3 |
+
from mapgs.model import MapGS
|
| 4 |
+
from mapgs.ttt import token_tuning, map_guided_densify
|
| 5 |
+
from mapgs.ttt.densify import anchors_along_trajectory
|
| 6 |
+
from .conftest import requires_cuda
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
@requires_cuda
|
| 10 |
+
def test_token_tuning_runs(cfg, dataset):
|
| 11 |
+
model = MapGS(cfg).cuda()
|
| 12 |
+
g = token_tuning(model, dataset[0], cfg, steps=3)
|
| 13 |
+
M = (cfg.model.tokens.n_map + cfg.model.tokens.n_free) * cfg.model.tokens.gaussians_per_token
|
| 14 |
+
assert g.means.shape == (1, M, 3)
|
| 15 |
+
# network weights must be unchanged by TT (only tokens are tuned)
|
| 16 |
+
before = {n: p.clone() for n, p in model.named_parameters()}
|
| 17 |
+
token_tuning(model, dataset[0], cfg, steps=2)
|
| 18 |
+
for n, p in model.named_parameters():
|
| 19 |
+
assert torch.allclose(before[n], p)
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
@requires_cuda
|
| 23 |
+
def test_densify_increases_gaussians(cfg, dataset):
|
| 24 |
+
model = MapGS(cfg).cuda()
|
| 25 |
+
s = dataset[0]
|
| 26 |
+
traj = torch.stack([torch.zeros(15, device="cuda"), torch.linspace(2, 25, 15, device="cuda")], -1)
|
| 27 |
+
npos, ntyp, nnrm = anchors_along_trajectory(s.ground.to("cuda"), traj, spacing=1.0)
|
| 28 |
+
n_new = min(64, npos.shape[0])
|
| 29 |
+
g = map_guided_densify(model, s, npos[:n_new], ntyp[:n_new], nnrm[:n_new], cfg)
|
| 30 |
+
base = (cfg.model.tokens.n_map + cfg.model.tokens.n_free) * cfg.model.tokens.gaussians_per_token
|
| 31 |
+
assert g.means.shape[1] == base + n_new * cfg.model.tokens.gaussians_per_token
|
tests/test_unified.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
|
| 3 |
+
import torch
|
| 4 |
+
|
| 5 |
+
from mapgs.config import load_config
|
| 6 |
+
from mapgs.geometry import resize_with_intrinsics
|
| 7 |
+
from mapgs.hdmap.ground_field import GridGroundField
|
| 8 |
+
from .conftest import requires_cuda, tiny_overrides
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def test_ground_field_from_raster_and_scaled():
|
| 12 |
+
gf = GridGroundField.from_raster(torch.ones(10, 12) * 2.0, x0=-3.0, y0=-2.0, dx=0.5)
|
| 13 |
+
z, valid = gf.height_at(torch.tensor([[0.0, 0.0]]))
|
| 14 |
+
assert valid.all() and abs(float(z[0]) - 2.0) < 1e-4
|
| 15 |
+
g2 = gf.scaled(0.5)
|
| 16 |
+
assert abs(g2.x0 - (-1.5)) < 1e-6 and abs(g2.dx - 0.25) < 1e-6
|
| 17 |
+
# scaling halves heights too (spatial)
|
| 18 |
+
z2, _ = g2.height_at(torch.tensor([[0.0, 0.0]]))
|
| 19 |
+
assert abs(float(z2[0]) - 1.0) < 1e-4
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def test_resize_with_intrinsics():
|
| 23 |
+
img = torch.rand(3, 100, 200)
|
| 24 |
+
K = torch.tensor([[200.0, 0, 100], [0, 200, 50], [0, 0, 1]])
|
| 25 |
+
out, Ks = resize_with_intrinsics(img, K, 50, 50)
|
| 26 |
+
assert out.shape == (3, 50, 50)
|
| 27 |
+
assert abs(float(Ks[0, 0]) - 50.0) < 1e-3 # fx * (50/200)
|
| 28 |
+
assert abs(float(Ks[1, 1]) - 100.0) < 1e-3 # fy * (50/100)
|
| 29 |
+
assert abs(float(Ks[0, 2]) - 25.0) < 1e-3 # cx * (50/200)
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
@requires_cuda
|
| 33 |
+
def test_unified_convert_load_and_mixed_roots(tmp_path):
|
| 34 |
+
cfg = load_config(overrides=tiny_overrides(str(tmp_path / "src")) + ["data.name=unified"])
|
| 35 |
+
from mapgs.data.convert import convert_synthetic
|
| 36 |
+
from mapgs.data import UnifiedClipDataset, collate_samples
|
| 37 |
+
|
| 38 |
+
ra = str(tmp_path / "ua"); rb = str(tmp_path / "ub")
|
| 39 |
+
convert_synthetic(cfg, ra, "train", n_clips=2, device="cuda")
|
| 40 |
+
convert_synthetic(cfg, rb, "train", n_clips=2, device="cuda")
|
| 41 |
+
|
| 42 |
+
# on-disk layout
|
| 43 |
+
clip = sorted(os.listdir(os.path.join(ra, "train")))[0]
|
| 44 |
+
files = os.listdir(os.path.join(ra, "train", clip))
|
| 45 |
+
assert "meta.pt" in files and "images" in files
|
| 46 |
+
|
| 47 |
+
ds = UnifiedClipDataset(cfg, roots=[ra, rb], split="train", n_sup_views=3)
|
| 48 |
+
assert len(ds) == 4 # mixed roots concatenated
|
| 49 |
+
s = ds[0]
|
| 50 |
+
assert s.ctx_images.shape[-2:] == (cfg.data.height, cfg.data.width)
|
| 51 |
+
assert s.anchor_pos.shape[0] == cfg.model.tokens.n_map
|
| 52 |
+
batch = collate_samples([ds[0], ds[1]])
|
| 53 |
+
assert batch["ctx_images"].shape[0] == 2
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
@requires_cuda
|
| 57 |
+
def test_unified_train_step(tmp_path):
|
| 58 |
+
cfg = load_config(overrides=tiny_overrides(str(tmp_path / "src")) + ["data.name=unified"])
|
| 59 |
+
from mapgs.data.convert import convert_synthetic
|
| 60 |
+
from mapgs.data import UnifiedClipDataset, collate_samples
|
| 61 |
+
from mapgs.train import Trainer
|
| 62 |
+
|
| 63 |
+
root = str(tmp_path / "u")
|
| 64 |
+
convert_synthetic(cfg, root, "train", n_clips=2, device="cuda")
|
| 65 |
+
ds = UnifiedClipDataset(cfg, roots=root, split="train", n_sup_views=3)
|
| 66 |
+
tr = Trainer(cfg)
|
| 67 |
+
log = tr.train_step(collate_samples([ds[0], ds[1]]))
|
| 68 |
+
assert log["total"] == log["total"] # finite
|