Source: tests/test_objectives.py · Plain text

tests/test_objectives.py

import math

import torch

from mcss.losses import MeasurementLoss
from mcss.metrics import compute_metrics


def _targets() -> dict[str, torch.Tensor]:
    return {
        "rgb": torch.full((1, 1, 3, 4, 4), 0.5),
        "depth": torch.full((1, 1, 1, 4, 4), 2.0),
        "normal": torch.tensor([0.0, 0.0, 1.0]).view(1, 1, 3, 1, 1).expand(1, 1, 3, 4, 4),
        "point": torch.zeros(1, 1, 3, 4, 4),
        "visibility": torch.ones(1, 1, 1, 4, 4),
        "uncertainty": torch.full((1, 1, 1, 4, 4), 0.1),
    }


def test_perfect_predictions_have_zero_geometry_error() -> None:
    targets = _targets()
    metrics = compute_metrics(targets, targets)

    assert math.isinf(metrics["rgb/psnr"])
    assert metrics["depth/abs_rel"] == 0.0
    assert metrics["depth/rmse"] == 0.0
    assert metrics["depth/delta1"] == 1.0
    assert metrics["normal/mean_angle"] < 0.03
    assert metrics["visibility/iou"] == 1.0
    assert metrics["visibility/f1"] == 1.0


def test_measurement_loss_masks_invalid_depth_and_is_differentiable() -> None:
    targets = _targets()
    targets["depth"][..., 0, 0] = 0.0
    predictions = {name: value.clone().requires_grad_(True) for name, value in targets.items()}
    predictions["depth"].data[..., 0, 0] = 100.0
    objective = MeasurementLoss({"rgb": 1.0, "depth": 1.0, "normal": 1.0, "visibility": 1.0})

    loss, terms = objective(predictions, targets)

    assert terms["depth"] == 0.0
    loss.backward()
    assert predictions["depth"].grad is not None


def test_visibility_loss_is_safe_inside_cuda_autocast() -> None:
    if not torch.cuda.is_available():
        return
    prediction = torch.full((1, 1, 1, 2, 2), 0.75, device="cuda", requires_grad=True)
    target = torch.ones_like(prediction)
    objective = MeasurementLoss({"visibility": 1.0})

    with torch.autocast(device_type="cuda", enabled=True):
        loss, terms = objective({"visibility": prediction}, {"visibility": target})
    loss.backward()

    assert torch.isfinite(loss)
    assert torch.isfinite(terms["visibility"])
    assert prediction.grad is not None


def test_support_mask_excludes_out_of_support_errors_from_every_loss() -> None:
    targets = _targets()
    support = torch.zeros_like(targets["visibility"])
    support[..., 0, 0] = 1.0
    targets["support"] = support
    predictions = {name: value.clone() for name, value in targets.items() if name != "support"}
    outside = ~support.bool()
    predictions["rgb"] = torch.where(outside.expand_as(predictions["rgb"]), 0.0, predictions["rgb"])
    predictions["depth"] = torch.where(outside, 100.0, predictions["depth"])
    predictions["normal"] = torch.where(
        outside.expand_as(predictions["normal"]),
        -predictions["normal"],
        predictions["normal"],
    )
    predictions["point"] = torch.where(
        outside.expand_as(predictions["point"]), 100.0, predictions["point"]
    )
    predictions["visibility"] = torch.where(outside, 0.0, predictions["visibility"])
    objective = MeasurementLoss(
        {"rgb": 1.0, "depth": 1.0, "normal": 1.0, "point": 1.0, "visibility": 1.0}
    )
    baseline, _ = objective(
        {name: target for name, target in targets.items() if name != "support"}, targets
    )

    loss, terms = objective(predictions, targets)

    torch.testing.assert_close(loss, baseline)
    assert terms["rgb"] < 0.002
    assert terms["depth"] == 0.0
    assert terms["normal"] == 0.0
    assert terms["point"] == 0.0


def test_metrics_report_support_coverage_counts_and_metric_point_scales() -> None:
    targets = _targets()
    support = torch.zeros_like(targets["visibility"])
    support[..., 0, 0] = 1.0
    targets["support"] = support
    predictions = {name: value.clone() for name, value in targets.items() if name != "support"}
    outside = ~support.bool()
    predictions["rgb"] = torch.where(outside.expand_as(predictions["rgb"]), 0.0, predictions["rgb"])
    predictions["depth"] = torch.where(outside, 100.0, predictions["depth"])
    predictions["point"] = torch.where(
        outside.expand_as(predictions["point"]), 100.0, predictions["point"]
    )
    bounds = torch.tensor([[[-4.0, -3.0, 0.1], [4.0, 3.0, 8.1]]])

    metrics = compute_metrics(predictions, targets, bounds=bounds)

    assert metrics["support/coverage"] == 1.0 / 16.0
    assert metrics["support/valid_pixels"] == 1.0
    assert metrics["support/total_pixels"] == 16.0
    assert metrics["depth/valid_pixels"] == 1.0
    assert metrics["depth/rmse"] == 0.0
    assert metrics["point/valid_points"] == 1.0
    assert metrics["point/rmse_m"] == 0.0
    assert metrics["point/nrmse_support_diag"] == 0.0
    assert metrics["point/fscore_0.05"] == 1.0
    assert metrics["point/fscore_0.25"] == 1.0
    assert metrics["point/fscore_0.50"] == 1.0


def test_ssim_distinguishes_local_structure_with_identical_global_moments() -> None:
    target = torch.full((1, 1, 3, 16, 16), 0.5)
    y, x = torch.meshgrid(torch.arange(16), torch.arange(16), indexing="ij")
    checkerboard = ((x + y) % 2).float()
    split = torch.zeros(16, 16)
    split[:8] = 1.0
    checkerboard = checkerboard.view(1, 1, 1, 16, 16).expand_as(target)
    split = split.view(1, 1, 1, 16, 16).expand_as(target)
    torch.testing.assert_close(checkerboard.mean(), split.mean())
    torch.testing.assert_close(checkerboard.var(unbiased=False), split.var(unbiased=False))

    checkerboard_ssim = compute_metrics({"rgb": checkerboard}, {"rgb": target})["rgb/ssim"]
    split_ssim = compute_metrics({"rgb": split}, {"rgb": target})["rgb/ssim"]

    assert not math.isclose(checkerboard_ssim, split_ssim, abs_tol=1e-3)


def test_point_cloud_metrics_sample_errors_across_the_full_point_map() -> None:
    target = torch.zeros(1, 1, 3, 64, 64)
    prediction = target.clone()
    prediction[:, :, 2, 32:, :] = 2.0
    visibility = torch.ones(1, 1, 1, 64, 64)

    metrics = compute_metrics(
        {"point": prediction},
        {"point": target, "visibility": visibility},
    )

    assert metrics["point/valid_points"] == 4096.0
    assert metrics["point/chamfer_m2"] > 0.5
    assert metrics["point/fscore_0.50"] < 0.9