Source: tests/test_geometry_certificates.py · Plain text

tests/test_geometry_certificates.py

import inspect
import json
from dataclasses import asdict

import pytest
import torch

from mcss.geometry_certificates import (
    CertificateConfig,
    GeometryCertificate,
    build_certificates,
    fundamental_matrix,
    triangulate_dlt,
)
from mcss.types import Cameras


def _cameras() -> Cameras:
    intrinsics = torch.tensor(
        [[[80.0, 0.0, 31.5], [0.0, 80.0, 31.5], [0.0, 0.0, 1.0]]]
    ).repeat(3, 1, 1)
    poses = torch.eye(4).repeat(3, 1, 1)
    poses[1, 0, 3] = 0.5
    poses[2, 1, 3] = 0.5
    return Cameras(intrinsics, poses, (64, 64))


def test_builder_signature_cannot_accept_labels() -> None:
    parameters = set(inspect.signature(build_certificates).parameters)

    assert parameters == {"context_rgb", "context_cameras", "config"}
    assert not any(name in parameters for name in ("depth", "target_rgb", "normal", "point"))


def test_fundamental_matrix_obeys_epipolar_constraint() -> None:
    cameras = _cameras()
    point = torch.tensor([[0.1, 0.2, 3.0]])
    pixels = []
    for view in range(2):
        w2c = torch.linalg.inv(cameras.c2w[view])
        camera_point = point @ w2c[:3, :3].T + w2c[:3, 3]
        projected = camera_point @ cameras.intrinsics[view].T
        pixels.append(projected[:, :2] / projected[:, 2:])
    homogeneous_0 = torch.cat((pixels[0], torch.ones(1, 1)), dim=-1)
    homogeneous_1 = torch.cat((pixels[1], torch.ones(1, 1)), dim=-1)

    matrix = fundamental_matrix(cameras, 0, 1)

    residual = homogeneous_1 @ matrix @ homogeneous_0.T
    torch.testing.assert_close(residual, torch.zeros_like(residual), atol=1e-5, rtol=0)


def test_dlt_triangulation_recovers_metric_point_and_positive_depth() -> None:
    cameras = _cameras()
    point = torch.tensor([0.1, 0.2, 3.0])
    observations = []
    for view in range(2):
        projection = cameras.intrinsics[view] @ torch.linalg.inv(cameras.c2w[view])[:3]
        homogeneous = projection @ torch.cat((point, torch.ones(1)))
        observations.append(homogeneous[:2] / homogeneous[2])

    result = triangulate_dlt(observations[0], observations[1], cameras, 0, 1)

    torch.testing.assert_close(result.point, point, atol=1e-4, rtol=1e-4)
    assert result.cheirality
    assert result.reprojection_error_px < 1e-4
    assert result.triangulation_angle_deg > 1.0


def test_certificate_is_json_serializable_and_frozen() -> None:
    certificate = GeometryCertificate(
        source_view=0,
        source_xy=(10.0, 12.0),
        matched_view=1,
        matched_xy=(11.0, 12.0),
        verification_view=2,
        verification_xy=(10.5, 12.5),
        point=(0.0, 0.0, 3.0),
        descriptor_distance=0.1,
        reciprocal_error_px=0.0,
        cycle_error_px=0.0,
        reprojection_error_px=0.1,
        verification_error_px=0.2,
        triangulation_angle_deg=5.0,
    )

    assert json.loads(json.dumps(asdict(certificate)))["point"] == [0.0, 0.0, 3.0]
    with pytest.raises((AttributeError, TypeError)):
        certificate.point = (1.0, 0.0, 3.0)  # type: ignore[misc]


def test_config_rejects_nonpositive_or_inconsistent_thresholds() -> None:
    with pytest.raises(ValueError, match="grid_stride"):
        CertificateConfig(grid_stride=0)
    with pytest.raises(ValueError, match="verification"):
        CertificateConfig(reprojection_threshold_px=2.0, verification_threshold_px=1.0)