- Unified reference depth calculation to use a single implementation in common/depth.py. - Removed server-side reference depth computation and adjusted depth.json to only include image_size and intrinsics. - Updated depth texture handling to ignore transparent, invalid, and non-positive pixels when calculating reference depth. - Changed reference depth calculation to use the 95th percentile of valid depth values. - Adjusted depth direction fitting to return a flat direction when no samples are available. - Removed cleanup threshold parameters from Cutout and related nodes, simplifying the interface. - Updated tests to verify new reference depth logic and ensure compatibility with existing functionality.
191 lines
8.9 KiB
Python
191 lines
8.9 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),
|
|
"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"
|
|
image.pixels.foreach_set(np.ones(8, dtype=np.float32))
|
|
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,sharing", [
|
|
("PLANE", "none"),
|
|
("PLANE", "material"),
|
|
("DEPTH", "visible"),
|
|
])
|
|
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,failure,entry", [
|
|
(False, "material", "PLANE"),
|
|
(True, "modifier", "DEPTH"),
|
|
(False, "finalize", "PLANE"),
|
|
])
|
|
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,kind", [(False, "generated"), (True, "file")])
|
|
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)
|