Files
AnyImage/tests/anyimage/server/test_panorama.py
nan 938d95db0f Add new functionality for image rectification and depth metadata handling
- Implemented a preview tool for rectifying images with interactive aspect controls in `preview.py`.
- Added tests for depth metadata loading to ensure consistent schema usage in `test_depth_metadata.py`.
- Developed comprehensive tests for cutout calibration processes in `test_cutout_calibration.py`, including stability checks and depth calibration.
- Created tests for frame compositing operations in `test_frame_compositing.py`, ensuring correct handling of depth and transparency.
- Introduced material slot inheritance functionality in `material_slots.py` to manage geometry material assignments.
- Built shared shader nodes for material processing in `shader.py`, including roughness and bump mapping setups.
2026-09-12 19:20:06 +08:00

134 lines
6.0 KiB
Python

"""Analytic projection, production inference and artifact lifecycle checks."""
import json
from types import SimpleNamespace
import numpy as np
import pytest
from PIL import Image
from tests.anyimage.server.support import FakeJobContext, load_server_module
geometry = load_server_module("geometry.panorama")
onnx = load_server_module("models.onnx_moge2")
job = load_server_module("jobs.panorama")
def test_known_fov_recovers_shift_metric_and_excludes_invisible_points():
rays = geometry.view_rays(64)
depth = 2 + 0.3 * rays[..., 0]
points = rays * depth[..., None]
points[..., 2] -= 0.7
visible = np.ones((64, 64), bool)
visible[:8] = False
points[:8] = np.nan
raw = {"points": points[None], "normal": np.zeros_like(points)[None],
"mask": np.ones((1, 64, 64)), "metric_scale": [1.5]}
prediction = onnx.postprocess(raw, fov_x=90, source_valid=visible)
np.testing.assert_allclose(prediction["depth"][visible], depth[visible] * 1.5, rtol=1e-5)
assert not prediction["mask"][:8].any()
assert np.isfinite(prediction["points"]).all()
def test_periodic_fusion_removes_view_scale_offsets():
bases = geometry.camera_bases()
distances = []
for scale, basis in zip(np.exp(np.linspace(-0.4, 0.4, 12)), bases):
rays = geometry.view_rays(128) @ basis
rays /= np.linalg.norm(rays, axis=-1, keepdims=True)
distances.append((3 + 0.4 * rays[..., 0] + 0.2 * rays[..., 2]) * scale)
merged, valid = geometry.merge_distances(distances, np.ones((12, 128, 128), bool), bases, 64, 32)
rays = geometry.panorama_directions(64, 32)
expected = 3 + 0.4 * rays[..., 0] + 0.2 * rays[..., 2]
merged *= np.median(expected / merged)
assert valid.all()
assert np.mean(abs(merged / expected - 1)) < 0.002
assert np.max(abs(merged[:, [0, -1]] / expected[:, [0, -1]] - 1)) < 0.005
sampled = geometry.sample_panorama(expected, np.array(((1, 1e-8, 0), (1, -1e-8, 0))))
np.testing.assert_allclose(sampled[0], sampled[1], atol=1e-6)
@pytest.fixture
def panorama_job(tmp_path, monkeypatch):
source = tmp_path / "source.png"
context = FakeJobContext(tmp_path / "result", "generate-panorama-geometry",
SimpleNamespace(directory=lambda _: tmp_path, get_moge=lambda *_: object()))
calls, pixels = [], []
def infer(session, color, level, **options):
calls.append((color, level, options))
rays = geometry.view_rays(color.shape[0])
points = rays / np.linalg.norm(rays, axis=-1, keepdims=True) * 3
return {"points": points, "mask": np.ones(color.shape[:2])}
monkeypatch.setattr(job, "infer", infer)
write = job.write_float_exr
monkeypatch.setattr(job, "write_float_exr", lambda rgba, path: (pixels.append(rgba.copy()), write(rgba, path))[1])
parameters = {"input": str(source), "model": "MOGE2", "device": "cpu", "resolution_level": 3,
"max_input_size": 32}
return source, context, parameters, calls, pixels
def test_job_outputs_geometry_combines_alpha_once_and_skips_transparent_views(panorama_job):
source, context, parameters, calls, pixels = panorama_job
image = np.full((32, 64, 4), 255, np.uint8)
image[:24, :, 3] = 0
image[26:28, :, 3] = 128
Image.fromarray(image).save(source)
result = job.run(context, parameters)
assert 0 < len(calls) < 12
assert all(level == 3 and opts["fov_x"] == 90 for _, level, opts in calls)
assert all(np.all(color[~opts["source_valid"]] == 0) for color, _, opts in calls)
assert {path.name for path in context.directory.iterdir()} == {"depth.exr", "depth.json"}
alpha = pixels[0][..., 3]
assert pixels[0].shape == image.shape
assert not alpha[:24].any()
assert np.all(alpha <= image[..., 3] / 255 + 1e-7)
assert np.any(np.isclose(alpha, 128 / 255))
np.testing.assert_allclose(pixels[0][..., 0], pixels[0][..., 2])
metadata = json.loads((context.directory / result["depth_metadata"]).read_text())
assert metadata["projection"] == "equirectangular"
assert metadata["image_size"] == [64, 32]
assert set(metadata) == {"projection", "image_size"}
def test_full_resolution_fusion_preserves_detail_and_periodic_alignment():
bases = geometry.camera_bases()
distances = []
def radius(rays):
angle = np.arctan2(rays[..., 1], rays[..., 0])
return 3 + 0.15 * np.sin(80 * angle) * (1 - rays[..., 2] ** 2) ** 4
for scale, basis in zip(np.exp(np.linspace(-0.2, 0.2, 12)), bases):
rays = geometry.view_rays(512) @ basis
rays /= np.linalg.norm(rays, axis=-1, keepdims=True)
distances.append(radius(rays) * scale)
merged, valid = geometry.merge_distances(distances, np.ones((12, 512, 512), bool),
bases, 1024, 512)
expected = radius(geometry.panorama_directions(1024, 512))
merged *= np.median(expected / merged)
assert merged.shape == (512, 1024) and valid.all()
assert np.sqrt(np.mean((merged - expected) ** 2)) < 0.001
assert np.max(abs(merged[:, [0, -1]] / expected[:, [0, -1]] - 1)) < 0.01
@pytest.mark.parametrize("kind", ["transparent", "invalid_model", "wrong_ratio", "cancel"])
def test_failed_jobs_leave_no_partial_artifacts(panorama_job, monkeypatch, kind):
source, context, parameters, calls, _ = panorama_job
Image.new("RGBA", (64 if kind != "wrong_ratio" else 32, 32),
(255, 255, 255, 0 if kind == "transparent" else 255)).save(source)
if kind == "invalid_model":
monkeypatch.setattr(job, "infer", lambda *a, **k: {"points": np.ones((32, 32, 3)), "mask": np.zeros((32, 32))})
if kind == "cancel":
count = [0]
def cancel():
count[0] += 1
if count[0] == 6:
raise RuntimeError("Cancelled")
context.check_cancelled = cancel
with pytest.raises((ValueError, RuntimeError)):
job.run(context, parameters)
assert not list(context.directory.glob("depth.*"))
assert not (context.directory / "foreground.png").exists()
if kind == "transparent":
assert not calls