Source: tests/test_manifest_dataset.py · Plain text

tests/test_manifest_dataset.py

import json
from pathlib import Path

import numpy as np
import pytest
import torch
from PIL import Image

from mcss.config import DatasetConfig
from mcss.data.manifest import ManifestFrame, SceneManifest, save_manifest
from mcss.data.manifest_dataset import ManifestSceneDataset, _find_manifests
from mcss.engine import build_dataset
from mcss.geometry import generate_rays


def _write_minimal_manifest(root: Path, scene_id: str) -> None:
    root.mkdir(parents=True)
    frames = tuple(
        ManifestFrame(
            frame_id=index,
            rgb=f"rgb/{index:06d}.png",
            intrinsics=[[2.0, 0.0, 0.5], [0.0, 2.0, 0.5], [0.0, 0.0, 1.0]],
            c2w=np.eye(4).tolist(),
        )
        for index in range(3)
    )
    save_manifest(
        SceneManifest(
            scene_id,
            (2, 2),
            ((-1.0, -1.0, -1.0), (1.0, 1.0, 1.0)),
            frames,
            root,
        ),
        root / "manifest.json",
    )


def test_manifest_dataset_loads_and_resizes_modalities(tmp_path: Path) -> None:
    scene = tmp_path / "scene"
    (scene / "rgb").mkdir(parents=True)
    (scene / "depth").mkdir()
    (scene / "normal").mkdir()
    for index in range(3):
        Image.new("RGB", (4, 4), (20 + index, 30, 40)).save(scene / "rgb" / f"{index:06d}.png")
        np.save(scene / "depth" / f"{index:06d}.npy", np.full((4, 4), 2.0, dtype=np.float32))
        normal = np.zeros((4, 4, 3), dtype=np.float32)
        normal[..., 2] = 1.0
        np.save(scene / "normal" / f"{index:06d}.npy", normal)
    frames = tuple(
        ManifestFrame(
            frame_id=index,
            rgb=f"rgb/{index:06d}.png",
            intrinsics=[[4.0, 0.0, 1.5], [0.0, 4.0, 1.5], [0.0, 0.0, 1.0]],
            c2w=np.eye(4).tolist(),
            depth=f"depth/{index:06d}.npy",
            normal=f"normal/{index:06d}.npy",
        )
        for index in range(3)
    )
    save_manifest(
        SceneManifest("scene", (4, 4), ((-1.0, -1.0, -1.0), (1.0, 1.0, 1.0)), frames, scene),
        scene / "manifest.json",
    )

    dataset = ManifestSceneDataset(scene, context_views=2, target_views=1, image_size=(2, 2))
    example = dataset[0]

    assert example.context_rgb.shape == (2, 3, 2, 2)
    assert example.target_depth.shape == (1, 1, 2, 2)
    assert example.target_normal.shape == (1, 3, 2, 2)
    assert example.target_point.shape == (1, 3, 2, 2)
    assert example.target_visibility.all()
    assert example.target_cameras.intrinsics[0, 0, 0] == 2.0
    assert example.target_cameras.intrinsics[0, 0, 2] == 0.75


def test_manifest_discovery_uses_direct_scene_fast_path(tmp_path: Path, monkeypatch) -> None:
    expected = []
    for name in ("scene_b", "scene_a"):
        scene = tmp_path / name
        scene.mkdir()
        manifest = scene / "manifest.json"
        manifest.write_text("{}", encoding="utf-8")
        expected.append(manifest)

    def fail_recursive_scan(_self: Path, _pattern: str):
        raise AssertionError("direct scene manifests should not trigger a recursive scan")

    monkeypatch.setattr(Path, "rglob", fail_recursive_scan)

    assert _find_manifests(tmp_path) == sorted(expected)


def test_manifest_dataset_filters_exact_scene_id_allowlist(tmp_path: Path) -> None:
    for scene_id in ("scene_c", "scene_a", "scene_b"):
        _write_minimal_manifest(tmp_path / scene_id, scene_id)

    dataset = ManifestSceneDataset(
        tmp_path,
        context_views=2,
        target_views=1,
        scene_ids=("scene_b", "scene_a"),
    )

    assert tuple(manifest.scene_id for manifest in dataset.manifests) == ("scene_b", "scene_a")
    assert len(dataset.entries) == 2
    with pytest.raises(ValueError, match="scene_missing"):
        ManifestSceneDataset(
            tmp_path,
            context_views=2,
            target_views=1,
            scene_ids=("scene_a", "scene_missing"),
        )


def test_build_dataset_passes_scene_id_allowlist(tmp_path: Path) -> None:
    for scene_id in ("scene_a", "scene_b"):
        _write_minimal_manifest(tmp_path / scene_id, scene_id)
    config = DatasetConfig(
        name="manifest",
        root=str(tmp_path),
        image_size=(2, 2),
        context_views=2,
        target_views=1,
        scene_ids=("scene_b",),
    )

    dataset = build_dataset(config, seed=3)

    assert isinstance(dataset, ManifestSceneDataset)
    assert tuple(manifest.scene_id for manifest in dataset.manifests) == ("scene_b",)


def _write_local_protocol_fixture(root: Path) -> Path:
    scene = root / "local_scene"
    (scene / "rgb").mkdir(parents=True)
    (scene / "depth").mkdir()
    (scene / "normal").mkdir()
    rotation = np.array([[0.0, -1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]], dtype=np.float32)
    poses = []
    for translation in ([10.0, 0.0, 0.0], [10.0, 0.5, 0.0], [10.0, 1.0, 0.0]):
        pose = np.eye(4, dtype=np.float32)
        pose[:3, :3] = rotation
        pose[:3, 3] = translation
        poses.append(pose)
    frames = []
    for index, pose in enumerate(poses):
        Image.new("RGB", (2, 2), (20 + index, 30, 40)).save(scene / "rgb" / f"{index:06d}.png")
        depth_path = None
        normal_path = None
        if index == 2:
            np.save(scene / "depth" / f"{index:06d}.npy", np.full((2, 2), 2.0, np.float32))
            world_normal = np.zeros((2, 2, 3), dtype=np.float32)
            world_normal[..., 1] = 1.0
            np.save(scene / "normal" / f"{index:06d}.npy", world_normal)
            depth_path = f"depth/{index:06d}.npy"
            normal_path = f"normal/{index:06d}.npy"
        frames.append(
            ManifestFrame(
                frame_id=index,
                rgb=f"rgb/{index:06d}.png",
                intrinsics=[[2.0, 0.0, 0.5], [0.0, 2.0, 0.5], [0.0, 0.0, 1.0]],
                c2w=pose.tolist(),
                depth=depth_path,
                normal=normal_path,
            )
        )
    save_manifest(
        SceneManifest(
            "local_scene",
            (2, 2),
            ((100.0, 100.0, 100.0), (200.0, 200.0, 200.0)),
            tuple(frames),
            scene,
        ),
        scene / "manifest.json",
    )
    return scene


def test_context_local_protocol_ignores_manifest_and_target_geometry_for_support(
    tmp_path: Path,
) -> None:
    scene = _write_local_protocol_fixture(tmp_path)
    local_bounds = ((-2.0, -2.0, 0.1), (2.0, 2.0, 4.1))
    dataset = ManifestSceneDataset(
        scene,
        context_views=2,
        target_views=1,
        spatial_protocol="context_local_metric",
        local_bounds_m=local_bounds,
    )

    first = dataset[0]
    np.save(scene / "depth" / "000002.npy", np.full((2, 2), 200.0, np.float32))
    changed_target = dataset[0]

    torch.testing.assert_close(first.bounds, torch.tensor(local_bounds))
    torch.testing.assert_close(first.bounds, changed_target.bounds)
    torch.testing.assert_close(first.target_support, changed_target.target_support)
    assert first.target_visibility.all()
    assert not changed_target.target_visibility.any()
    assert not changed_target.target_depth.any()
    assert not changed_target.target_normal.any()
    assert not changed_target.target_point.any()
    assert not changed_target.target_rgb.any()
    assert first.scene_id == "local_scene"


def test_context_local_protocol_rebases_cameras_points_and_world_normals(tmp_path: Path) -> None:
    scene = _write_local_protocol_fixture(tmp_path)
    dataset = ManifestSceneDataset(
        scene,
        context_views=2,
        target_views=1,
        spatial_protocol="context_local_metric",
        local_bounds_m=((-2.0, -2.0, 0.1), (2.0, 2.0, 4.1)),
    )

    example = dataset[0]
    origins, directions = generate_rays(example.target_cameras)
    expected_points = origins + directions * example.target_depth[:, 0].unsqueeze(-1)

    torch.testing.assert_close(example.context_cameras.c2w[0], torch.eye(4), atol=1e-6, rtol=1e-6)
    torch.testing.assert_close(example.target_cameras.c2w[0, :3, 3], torch.tensor([1.0, 0.0, 0.0]))
    torch.testing.assert_close(
        example.target_point, expected_points.permute(0, 3, 1, 2), atol=1e-6, rtol=1e-6
    )
    expected_normal = (
        torch.tensor([1.0, 0.0, 0.0]).view(1, 3, 1, 1).expand_as(example.target_normal)
    )
    torch.testing.assert_close(example.target_normal, expected_normal, atol=1e-6, rtol=1e-6)
    assert example.target_support.shape == (1, 1, 2, 2)


def test_context_local_protocol_preserves_explicit_metric_point_magnitude(tmp_path: Path) -> None:
    scene = _write_local_protocol_fixture(tmp_path)
    (scene / "point").mkdir()
    world_points = np.empty((2, 2, 3), dtype=np.float32)
    world_points[...] = [10.0, 3.0, 2.0]
    np.save(scene / "point" / "000002.npy", world_points)
    manifest_path = scene / "manifest.json"
    manifest_data = json.loads(manifest_path.read_text(encoding="utf-8"))
    manifest_data["frames"][2]["point"] = "point/000002.npy"
    manifest_path.write_text(json.dumps(manifest_data), encoding="utf-8")
    dataset = ManifestSceneDataset(
        scene,
        context_views=2,
        target_views=1,
        spatial_protocol="context_local_metric",
        local_bounds_m=((-4.0, -2.0, 0.1), (4.0, 2.0, 4.1)),
    )

    example = dataset[0]

    expected = torch.tensor([3.0, 0.0, 2.0]).view(1, 3, 1, 1).expand_as(example.target_point)
    torch.testing.assert_close(example.target_point, expected, atol=1e-6, rtol=1e-6)