tests/test_v8_suppliers.py
from pathlib import Path
import numpy as np
import pytest
import torch
from mcss import v8_suppliers
from mcss.geometry_supplier import GeometrySupplierRequest
from mcss.types import Cameras
from mcss.v8_suppliers import EMVSNetSupplier, UniDepthV2SmallSupplier
def _request() -> GeometrySupplierRequest:
rgb = torch.rand(4, 3, 8, 10)
K = torch.tensor([[[8.0, 0.0, 4.5], [0.0, 8.0, 3.5], [0.0, 0.0, 1.0]]]).repeat(4, 1, 1)
c2w = torch.eye(4).repeat(4, 1, 1)
c2w[:, 0, 3] = torch.arange(4, dtype=torch.float32) * 0.1
return GeometrySupplierRequest((4, 1, 3, 2), rgb, Cameras(K, c2w, (8, 10)))
class _FakeUniDepth:
def parameters(self):
return iter(())
def infer(self, rgb, K):
assert tuple(rgb.shape) == (3, 8, 10)
assert tuple(K.shape) == (3, 3)
return {
"depth": torch.ones(1, 1, 8, 10),
"points": torch.ones(1, 3, 8, 10),
"confidence": torch.full((1, 1, 8, 10), 0.7),
}
def to(self, _device):
return self
def eval(self):
return self
class _FakeEMVSNet:
def __call__(self, images, projection, depth_values):
del projection
batch, _views, _channels, height, width = images.shape
depth_bins = depth_values.shape[1]
primary = torch.zeros(batch, depth_bins, height, width)
auxiliary = torch.zeros_like(primary)
primary[:, 7] = 1.0
auxiliary[:, 19] = 1.0
evidential = torch.zeros(batch, 4, height, width)
evidential[:, 0] = depth_values[:, 31, None, None]
evidential[:, 1] = 2.0
evidential[:, 2] = 3.0
evidential[:, 3] = 4.0
return primary, evidential, auxiliary
def test_unidepth_adapter_preserves_context_views_and_shapes(tmp_path: Path) -> None:
supplier = UniDepthV2SmallSupplier(code_root=tmp_path, device="cpu")
supplier._model = _FakeUniDepth()
result = supplier.infer(_request())
assert result.frame_ids == (1, 2, 3, 4)
assert result.depth_z_m.shape == (4, 8, 10)
assert result.points_cam_m.shape == (4, 8, 10, 3)
assert result.unit_provenance == "metric_m"
def test_emvsnet_requires_frozen_64_bin_checkpoint(tmp_path: Path) -> None:
with pytest.raises(ValueError, match="64-bin"):
EMVSNetSupplier(code_root=tmp_path, checkpoint=tmp_path / "x.ckpt", depth_bins=128)
def test_emvsnet_center_normalization_is_per_image_and_channel() -> None:
base = torch.arange(24, dtype=torch.float32).reshape(1, 3, 2, 4)
images = torch.cat([base, base * 3.0 + 17.0], dim=0)
normalized = v8_suppliers.center_emvsnet_images(images)
assert torch.allclose(normalized.mean(dim=(-2, -1)), torch.zeros(2, 3), atol=1e-6)
assert torch.allclose(
normalized.var(dim=(-2, -1), correction=0), torch.ones(2, 3), atol=1e-5
)
def test_emvsnet_depth_hypotheses_are_frozen_and_label_independent(tmp_path: Path) -> None:
supplier = EMVSNetSupplier(code_root=tmp_path, checkpoint=tmp_path / "x.ckpt")
values = supplier.depth_hypotheses_m(device=torch.device("cpu"))
assert values.shape == (64,)
assert values[0].item() == pytest.approx(v8_suppliers.EMVSNET_DEPTH_NEAR_M)
assert values[-1].item() == pytest.approx(v8_suppliers.EMVSNET_DEPTH_FAR_M)
def test_emvsnet_projection_is_equivariant_to_joint_metric_scaling() -> None:
request = _request().canonical()
depth_m = torch.tensor([0.2, 1.5, 7.0], dtype=torch.float32)
projection_m = v8_suppliers.emvsnet_projection_coordinates(
request.cameras, translation_scale=1.0
)
projection_mm = v8_suppliers.emvsnet_projection_coordinates(
request.cameras, translation_scale=1000.0
)
relative_m = projection_m[1] @ torch.linalg.inv(projection_m[0])
relative_mm = projection_mm[1] @ torch.linalg.inv(projection_mm[0])
pixel = torch.tensor([3.0, 2.0, 1.0], dtype=torch.float32)
points_m = relative_m[:3, :3] @ pixel[:, None] * depth_m[None]
points_m += relative_m[:3, 3:4]
points_mm = relative_mm[:3, :3] @ pixel[:, None] * (depth_m * 1000.0)[None]
points_mm += relative_mm[:3, 3:4]
assert torch.allclose(
points_m[:2] / points_m[2:3], points_mm[:2] / points_mm[2:3], atol=1e-5
)
def test_emvsnet_der_risk_uses_official_epistemic_formula() -> None:
evidential = torch.zeros(1, 4, 1, 1)
evidential[:, 1] = 2.0 # nu
evidential[:, 2] = 3.0 # alpha
evidential[:, 3] = 4.0 # beta
risk = v8_suppliers.emvsnet_epistemic_risk(evidential, method="der")
assert risk.item() == pytest.approx(1.0)
evidential[:, 1] = 4.0
lower_risk = v8_suppliers.emvsnet_epistemic_risk(evidential, method="der")
assert lower_risk.item() < risk.item()
def test_emvsnet_depth_uses_training_and_validation_probability_volume(tmp_path: Path) -> None:
request = _request()
request = type(request)(
request.frame_ids,
torch.nn.functional.interpolate(request.rgb, size=(128, 160), mode="bilinear"),
type(request.cameras)(
request.cameras.intrinsics,
request.cameras.c2w,
(128, 160),
),
)
supplier = EMVSNetSupplier(
code_root=tmp_path,
checkpoint=tmp_path / "x.ckpt",
device="cpu",
)
supplier._model = _FakeEMVSNet()
result = supplier.infer(request)
expected = supplier.depth_hypotheses_m(device=torch.device("cpu"))[7].item()
auxiliary = supplier.depth_hypotheses_m(device=torch.device("cpu"))[19].item()
assert np.allclose(result.depth_z_m[0], expected)
assert not np.allclose(result.depth_z_m[0], auxiliary)