- 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.
186 lines
9.0 KiB
Python
186 lines
9.0 KiB
Python
"""Verify conversion ownership, source recovery, and persistent color data."""
|
|
|
|
from types import SimpleNamespace
|
|
|
|
import bpy
|
|
import numpy as np
|
|
import pytest
|
|
from PIL import Image
|
|
|
|
from anyimage.common import image as images, material
|
|
from anyimage.common.color_image import prepare_material_color_input, cleanup_material_color_input
|
|
from anyimage.operators.convert_to_plane import operators, object as conversion, image_plane
|
|
|
|
|
|
@pytest.fixture
|
|
def source():
|
|
bpy.ops.wm.read_factory_settings(use_empty=True)
|
|
image = bpy.data.images.new("Source.png", width=2, height=1, alpha=True)
|
|
image.pixels.foreach_set([0.2, 0.4, 0.8, 0.25, 0.6, 0.3, 0.1, 0])
|
|
obj = bpy.data.objects.new("Source", None)
|
|
obj.empty_display_type = "IMAGE"
|
|
obj.data = image
|
|
bpy.context.collection.objects.link(obj)
|
|
obj.select_set(True)
|
|
bpy.context.view_layer.objects.active = obj
|
|
yield obj
|
|
bpy.ops.wm.read_factory_settings(use_empty=True)
|
|
|
|
|
|
def share_image(source, sharing):
|
|
if sharing in {"visible", "hidden", "other_scene", "same_path"}:
|
|
other = source.copy()
|
|
if sharing == "same_path":
|
|
source.data.filepath_raw = "//source.png"
|
|
other.data = source.data.copy()
|
|
collection = bpy.context.collection
|
|
if sharing == "other_scene":
|
|
collection = bpy.data.scenes.new("Other scene").collection
|
|
collection.objects.link(other)
|
|
other.hide_viewport = sharing == "hidden"
|
|
elif sharing == "material":
|
|
existing = bpy.data.materials.new("Existing")
|
|
existing.use_nodes = True
|
|
existing.node_tree.nodes.new("ShaderNodeTexImage").image = source.data
|
|
|
|
|
|
def run_conversion(source, entry, monkeypatch):
|
|
if entry == "PLANE":
|
|
operators.ConvertToPlane.execute(SimpleNamespace(mesh_detail=0), bpy.context)
|
|
else:
|
|
metadata = {"image_size": (2, 1), "reference_depth": 1,
|
|
"intrinsics": ((2, 0, 1), (0, 2, 0.5), (0, 0, 1))}
|
|
monkeypatch.setattr(conversion, "load_depth_metadata", lambda _: metadata)
|
|
def load_depth(*_):
|
|
image = bpy.data.images.new("Depth", width=2, height=1, float_buffer=True)
|
|
image.colorspace_settings.name = "Non-Color"
|
|
return image
|
|
monkeypatch.setattr(conversion, "load_depth_result_image", load_depth)
|
|
monkeypatch.setattr(conversion, "load_normal_result_image", load_depth)
|
|
monkeypatch.setattr(conversion, "bpy", SimpleNamespace(data=bpy.data,
|
|
ops=SimpleNamespace(ed=SimpleNamespace(undo_push=lambda **_: {"FINISHED"}))))
|
|
path = prepare_material_color_input(source.data)
|
|
try:
|
|
conversion.create_depth_plane_from_result(bpy.context, SimpleNamespace(file=lambda key: key),
|
|
SimpleNamespace(source_object_name=source.name, source_identity=str(source.as_pointer()),
|
|
image_identity=str(source.data.as_pointer()), color_path=str(path), plane_type=entry, mesh_detail=0))
|
|
finally:
|
|
cleanup_material_color_input(path)
|
|
return bpy.context.object
|
|
|
|
|
|
@pytest.mark.parametrize("entry", ["PLANE", "DEPTH", "RELIEF"])
|
|
@pytest.mark.parametrize("sharing", ["none", "visible", "hidden", "other_scene", "material", "same_path"])
|
|
def test_conversion_selects_source_by_empty_identity(source, monkeypatch, entry, sharing):
|
|
share_image(source, sharing)
|
|
original = source.data
|
|
before = images.image_pixels(original)
|
|
name, path = original.name, original.filepath_raw
|
|
monkeypatch.setattr(material, "configured_material_view_adaptation", lambda: False)
|
|
obj = run_conversion(source, entry, monkeypatch)
|
|
color = next(n.image for n in obj.data.materials[0].node_tree.nodes if n.type == "TEX_IMAGE")
|
|
assert color != original
|
|
assert color.name == "Source_color.png"
|
|
assert color.filepath_raw != original.filepath_raw
|
|
assert original.name == name and original.filepath_raw == path
|
|
assert color.colorspace_settings.name == "sRGB" and color.alpha_mode == "PREMUL"
|
|
assert original.alpha_mode == "STRAIGHT"
|
|
np.testing.assert_array_equal(images.image_pixels(original), before)
|
|
np.testing.assert_allclose(images.image_pixels(color), before, atol=1 / 255)
|
|
if sharing == "material":
|
|
existing = bpy.data.materials["Existing"]
|
|
assert next(n.image for n in existing.node_tree.nodes if n.type == "TEX_IMAGE") == original
|
|
|
|
|
|
@pytest.mark.parametrize("shared", [False, True])
|
|
@pytest.mark.parametrize("failure", ["material", "modifier", "finalize"])
|
|
@pytest.mark.parametrize("entry", ["PLANE", "DEPTH", "RELIEF"])
|
|
def test_conversion_failure_restores_image_and_releases_resources(source, monkeypatch, shared, failure, entry):
|
|
if shared:
|
|
share_image(source, "visible")
|
|
source.data.pack()
|
|
source.data.pixels[0] = 0.75
|
|
original = source.data
|
|
state = images.image_content_state(original)
|
|
before = {name: set(getattr(bpy.data, name)) for name in ("images", "objects", "meshes", "materials")}
|
|
def fail(*args, **kwargs):
|
|
raise RuntimeError("Conversion failed")
|
|
monkeypatch.setattr(material, "configured_material_view_adaptation", lambda: True)
|
|
if failure == "material":
|
|
monkeypatch.setattr(material, "material_node_group", fail)
|
|
elif failure == "finalize":
|
|
monkeypatch.setattr(conversion, "finalize_object_result", fail)
|
|
elif entry == "PLANE":
|
|
monkeypatch.setattr(image_plane, "add_image_plane_processing", fail)
|
|
else:
|
|
monkeypatch.setattr(conversion, "set_modifier_input", fail)
|
|
with pytest.raises(RuntimeError, match="Conversion failed"):
|
|
run_conversion(source, entry, monkeypatch)
|
|
for name, values in before.items():
|
|
assert set(getattr(bpy.data, name)) == values
|
|
assert source.data == original
|
|
assert (original.colorspace_settings.name, original.alpha_mode) == (state["colorspace"], state["alpha_mode"])
|
|
assert bytes(original.packed_file.data) == state["packed"]
|
|
np.testing.assert_array_equal(original.pixels[:], state["pixels"])
|
|
|
|
|
|
@pytest.mark.parametrize("shared", [False, True])
|
|
@pytest.mark.parametrize("kind", ["generated", "file", "dirty", "packed"])
|
|
def test_conversion_pixels_survive_library_reload(source, tmp_path, monkeypatch, kind, shared):
|
|
if kind in {"file", "dirty"}:
|
|
path = tmp_path / "source.png"
|
|
Image.fromarray(np.array([[[51, 102, 204, 64], [153, 76, 25, 0]]], np.uint8)).save(path)
|
|
source.data = bpy.data.images.load(str(path))
|
|
if kind == "dirty":
|
|
source.data.pixels[0] = 0.8
|
|
elif kind == "packed":
|
|
source.data.pack()
|
|
source.data.reload()
|
|
if shared:
|
|
share_image(source, "visible")
|
|
expected = images.image_pixels(source.data)
|
|
monkeypatch.setattr(material, "configured_material_view_adaptation", lambda: True)
|
|
obj = run_conversion(source, "PLANE", monkeypatch)
|
|
color = next(n.image for n in obj.data.materials[0].node_tree.nodes if n.type == "TEX_IMAGE")
|
|
library = tmp_path / "converted.blend"
|
|
bpy.data.libraries.write(str(library), {color})
|
|
with bpy.data.libraries.load(str(library)) as (saved, loaded):
|
|
loaded.images = saved.images
|
|
restored = loaded.images[0]
|
|
assert (restored.colorspace_settings.name, restored.alpha_mode) == (color.colorspace_settings.name, color.alpha_mode)
|
|
np.testing.assert_allclose(images.image_pixels(restored), expected, atol=1 / 255)
|
|
|
|
|
|
@pytest.mark.parametrize("entry", ["DEPTH", "RELIEF"])
|
|
def test_depth_result_rejects_replaced_source_image(source, entry):
|
|
operator = SimpleNamespace(source_object_name=source.name, source_identity=str(source.as_pointer()),
|
|
image_identity=str(source.data.as_pointer()), plane_type=entry)
|
|
source.data = source.data.copy()
|
|
with pytest.raises(ValueError, match="source image has changed"):
|
|
conversion.create_depth_plane_from_result(bpy.context, None, operator)
|
|
|
|
|
|
@pytest.mark.parametrize("shared", [False, True])
|
|
def test_plane_undo_redo_restores_image_configuration(source, monkeypatch, shared):
|
|
if shared:
|
|
share_image(source, "visible")
|
|
original_name = source.data.name
|
|
expected = images.image_pixels(source.data)
|
|
monkeypatch.setattr(material, "configured_material_view_adaptation", lambda: False)
|
|
bpy.context.preferences.edit.use_global_undo = True
|
|
bpy.utils.register_class(operators.ConvertToPlane)
|
|
try:
|
|
bpy.ops.ed.undo_push(message="Before Plane")
|
|
assert bpy.ops.anyimage.convert_to_plane("EXEC_DEFAULT", True) == {"FINISHED"}
|
|
color_name = "Source_color.png"
|
|
assert bpy.data.images[color_name].alpha_mode == "PREMUL"
|
|
assert bpy.ops.ed.undo() == {"FINISHED"}
|
|
assert bpy.data.objects["Source"].type == "EMPTY"
|
|
assert bpy.data.images[original_name].alpha_mode == "STRAIGHT"
|
|
np.testing.assert_allclose(images.image_pixels(bpy.data.images[original_name]), expected, atol=1 / 255)
|
|
assert bpy.ops.ed.redo() == {"FINISHED"}
|
|
assert bpy.data.objects["Source"].type == "MESH"
|
|
assert bpy.data.images[color_name].alpha_mode == "PREMUL"
|
|
finally:
|
|
bpy.utils.unregister_class(operators.ConvertToPlane)
|