Files
AnyImage/tests/anyimage/server/test_panorama.py
nan 489003c2cc Add new tests and functionality for image processing and geometry handling
- Introduced color_image.py for preparing standalone color images for tests.
- Added hdr_image.py to create HDR textures for testing.
- Implemented test_image_depth_panorama.py to evaluate panorama topology, UVs, and depth discontinuities.
- Created test_shadeless.py to validate the opacity and HDR color endpoints of the Shadeless group.
- Developed moge3_export.py for exporting dynamic MoGe-3 backbone and ONNX graphs.
- Added material.py to inherit material slots onto generated geometry.
- Built image_depth_panorama.py to construct a quad panorama with depth separation and sampling.
2026-09-10 14:42:17 +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, "video_frames": 1, "start_frame": 1}
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 metadata["reference_distance"] == pytest.approx(3, rel=1e-5)
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 / "image.0001.png").exists()
if kind == "transparent":
assert not calls