Files
AnyImage/tests/anyimage/operators/test_plane_operators.py
nan 2dfa9b2add Refactor depth reference handling and cleanup
- 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.
2026-09-15 22:50:16 +08:00

192 lines
9.1 KiB
Python

from pathlib import Path
from contextlib import nullcontext
from types import SimpleNamespace
from unittest.mock import Mock, patch
from tests.support.blender import BlenderTestCase
class PlaneOperatorsTest(BlenderTestCase):
def test_plane_rejects_animated_images_before_creating_resources(self):
for kind in ("MOVIE", "SEQUENCE"):
for duration in (1, 24):
source = SimpleNamespace(data=SimpleNamespace(source=kind, frame_duration=duration))
operator = SimpleNamespace(report=Mock())
with patch.object(self.convert_to_plane, "material_color_image") as prepare:
result = self.convert_to_plane.ConvertToPlane.execute(
operator, SimpleNamespace(object=source))
self.assertEqual(result, {"CANCELLED"})
prepare.assert_not_called()
operator.report.assert_called_once_with({"ERROR"}, "Material conversion supports static images only")
def test_depth_plane_rejects_animation_and_requests_ai_setup(self):
operator = SimpleNamespace(mesh_detail=6, report=Mock())
context = SimpleNamespace(object=SimpleNamespace(data=object()))
with (
patch.object(self.convert_to_plane, "require_static_color_image", side_effect=ValueError("Material conversion supports static images only")),
patch.object(self.convert_to_plane, "invoke_ai_setup_if_needed") as setup,
):
self.assertEqual(self.convert_to_plane.ConvertToDepthPlane.execute(operator, context), {"CANCELLED"})
setup.assert_not_called()
with (
patch.object(self.convert_to_plane, "require_static_color_image"),
patch.object(self.convert_to_plane, "invoke_ai_setup_if_needed", return_value={"FINISHED"}) as setup,
patch.object(self.convert_to_plane, "prepare_material_color_input") as prepare,
):
self.assertEqual(self.convert_to_plane.ConvertToDepthPlane.execute(operator, context), {"FINISHED"})
setup.assert_called_once()
prepare.assert_not_called()
def test_depth_and_relief_start_jobs_with_their_plane_type(self):
for plane_type, operator_class in (("DEPTH", self.convert_to_plane.ConvertToDepthPlane), ("RELIEF", self.convert_to_plane.ConvertToReliefPlane)):
module = self.convert_to_plane
source = SimpleNamespace(name="Source", as_pointer=lambda: 1,
data=SimpleNamespace(as_pointer=lambda: 2))
context = SimpleNamespace(object=source, scene=object())
operator = SimpleNamespace(mesh_detail=4, report=Mock())
generate = Mock(return_value={"FINISHED"})
with (
patch.object(module, "invoke_ai_setup_if_needed", return_value=None),
patch.object(module, "prepare_material_color_input", return_value=Path("input.png")),
patch.object(module, "material_analysis_input", return_value=Path("input.png")),
patch.object(module, "depth_plane_settings", return_value=("moge", 5)),
patch.object(module, "configured_max_ai_input_size", return_value=1024),
patch.object(module.bpy, "ops", SimpleNamespace(anyimage=SimpleNamespace(generate_depth_plane=generate)), create=True),
):
self.assertEqual(operator_class.execute(operator, context), {"FINISHED"})
self.assertEqual(generate.call_args.kwargs["source_object_name"], "Source")
self.assertEqual(generate.call_args.kwargs["plane_type"], plane_type)
def test_depth_and_relief_results_use_their_normal_space(self):
for plane_type, normal_space in (("DEPTH", "OBJECT"), ("RELIEF", "TANGENT")):
normal_key = f"{normal_space.lower()}_normal"
module = self.convert_to_plane_object
source = SimpleNamespace(data=SimpleNamespace(name="Source"))
color_image = SimpleNamespace(name="", users=0)
depth_image = SimpleNamespace(users=0)
normal_image = SimpleNamespace(users=0)
material = SimpleNamespace(users=0)
metadata = {
"image_size": (2, 1),
"intrinsics": ((1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 1.0)),
}
files = {
"depth_metadata": Path("depth.json"),
"depth": Path("depth.exr"),
normal_key: Path(f"{normal_key}.png"),
}
result = SimpleNamespace(
directory=Path("result"),
file=lambda key: files[key],
)
operator = SimpleNamespace(source_object_name="Source", color_path="color.png", mesh_detail=6, plane_type=plane_type)
context = SimpleNamespace(scene=object())
ops = SimpleNamespace(
ed=SimpleNamespace(undo_push=Mock(return_value={"FINISHED"}))
)
with (
patch.object(module, "require_conversion_source", return_value=source),
patch.object(module, "material_color_image", return_value=nullcontext(color_image)),
patch.object(module, "load_depth_metadata", return_value=metadata),
patch.object(module, "load_depth_result_image", return_value=depth_image) as load_depth,
patch.object(
module,
"load_normal_result_image",
return_value=normal_image,
) as load_normal,
patch.object(
module,
"create_image_material",
return_value=material,
) as create_material,
patch.object(module, "create_depth_plane_object") as create_object,
patch.object(module.bpy, "ops", ops, create=True),
):
module.create_depth_plane_from_result(context, result, operator)
load_depth.assert_called_once_with(files["depth"], source, metadata)
ops.ed.undo_push.assert_called_once_with(message=f"Convert to {plane_type.title()} Plane")
load_normal.assert_called_once_with(files[normal_key], source)
create_material.assert_called_once_with(
source.data,
color_image,
normal_image=normal_image,
normal_space=normal_space,
scene=context.scene,
)
create_object.assert_called_once_with(
context,
source,
6,
material,
depth_image,
metadata,
plane_type,
)
def test_depth_plane_result_cleans_unused_depth_and_normal_on_failure(self):
module = self.convert_to_plane_object
source = SimpleNamespace(data=SimpleNamespace(name="Source"))
color_image = SimpleNamespace(name="", users=0)
depth_image = SimpleNamespace(users=0)
normal_image = SimpleNamespace(users=0)
material = SimpleNamespace(users=0)
files = {
"depth_metadata": Path("depth.json"),
"depth": Path("depth.exr"),
"object_normal": Path("object-normal.png"),
}
result = SimpleNamespace(
directory=Path("result"),
file=lambda key: files[key],
)
remove_material = Mock()
remove_image = Mock()
data = SimpleNamespace(
materials=SimpleNamespace(remove=remove_material),
images=SimpleNamespace(remove=remove_image),
)
with (
patch.object(module, "require_conversion_source", return_value=source),
patch.object(module, "material_color_image", return_value=nullcontext(color_image)),
patch.object(
module,
"load_depth_metadata",
return_value={
"image_size": (2, 1),
"intrinsics": ((1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 1.0)),
},
),
patch.object(module, "load_depth_result_image", return_value=depth_image),
patch.object(module, "load_normal_result_image", return_value=normal_image),
patch.object(module, "create_image_material", return_value=material),
patch.object(
module,
"create_depth_plane_object",
side_effect=RuntimeError("failed"),
),
patch.object(module.bpy, "data", data),
self.assertRaisesRegex(RuntimeError, "failed"),
):
module.create_depth_plane_from_result(
SimpleNamespace(scene=object()),
result,
SimpleNamespace(source_object_name="Source", color_path="color.png", mesh_detail=6, plane_type="DEPTH"),
)
remove_material.assert_called_once_with(material, do_unlink=True)
self.assertEqual(
[entry.args[0] for entry in remove_image.call_args_list],
[depth_image, normal_image],
)
self.assertTrue(
all(
entry.kwargs == {"do_unlink": True}
for entry in remove_image.call_args_list
)
)