Files
AnyImage/tests/test_image_depth.py
nan a503786c62 Refactor and enhance SnapForm functionality
- Introduced MODEL_NUMBERS for better model indexing.
- Improved enum_items function to cache results and handle download status.
- Updated ActiveJob class to manage temporary input paths and added displacement_via property.
- Changed IMAGE_DEPTH_PROCESS_RES_METHOD from "lower_bound_resize" to "upper_bound_resize".
- Enhanced RunJob operator to include new properties and improved error messages.
- Modified panel layout for better user experience and added runtime installation checks.
- Updated SnapFormSettings to reflect new model properties and defaults.
- Added temporary input path management in runtime.
- Refined worker functions to improve error handling and messaging.
- Expanded tests to cover new functionality and ensure backward compatibility.
- Fixed packaging script to align with versioning conventions.
2026-08-03 13:47:15 +08:00

470 lines
16 KiB
Python

import importlib.util
import inspect
import sys
import tempfile
import unittest
from pathlib import Path
from types import ModuleType, SimpleNamespace
from unittest.mock import patch
import numpy as np
PROJECT_ROOT = Path(__file__).resolve().parents[1]
def load_image_depth():
fake_bpy = ModuleType("bpy")
fake_bpy.props = SimpleNamespace(
BoolProperty=lambda **options: options,
EnumProperty=lambda **options: options,
FloatProperty=lambda **options: options,
IntProperty=lambda **options: options,
StringProperty=lambda **options: options,
)
fake_bpy.types = SimpleNamespace(Operator=object)
fake_bpy.path = SimpleNamespace(abspath=lambda path: path)
with patch.dict(sys.modules, {"bpy": fake_bpy}):
specification = importlib.util.spec_from_file_location(
"snapform_image_depth_test",
PROJECT_ROOT / "src" / "snapform" / "image_depth.py",
)
module = importlib.util.module_from_spec(specification)
specification.loader.exec_module(module)
return module
class ImageDepthTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.image_depth = load_image_depth()
def test_extracts_depth_and_uses_first_view(self):
with tempfile.TemporaryDirectory() as directory:
npz_path = Path(directory) / "results.npz"
np.savez_compressed(
npz_path,
depth=np.array(
(
((1.0, 2.0), (3.0, 4.0)),
((5.0, 6.0), (7.0, 8.0)),
),
dtype=np.float32,
),
)
depth = self.image_depth.extract_depth_array(npz_path)
np.testing.assert_array_equal(
depth,
np.array(((1.0, 2.0), (3.0, 4.0)), dtype=np.float32),
)
def test_accepts_depths_key_with_single_channel(self):
with tempfile.TemporaryDirectory() as directory:
npz_path = Path(directory) / "results.npz"
np.savez_compressed(
npz_path,
depths=np.ones((1, 2, 3, 1), dtype=np.float32),
)
depth = self.image_depth.extract_depth_array(npz_path)
self.assertEqual(depth.shape, (2, 3))
def test_accepts_single_depth_map_with_channel_axis(self):
with tempfile.TemporaryDirectory() as directory:
npz_path = Path(directory) / "results.npz"
np.savez_compressed(
npz_path,
depth=np.ones((2, 3, 1), dtype=np.float32),
)
depth = self.image_depth.extract_depth_array(npz_path)
self.assertEqual(depth.shape, (2, 3))
def test_extracts_camera_intrinsics(self):
expected = np.array(
((100.0, 0.0, 50.0), (0.0, 120.0, 40.0), (0.0, 0.0, 1.0)),
dtype=np.float32,
)
with tempfile.TemporaryDirectory() as directory:
npz_path = Path(directory) / "results.npz"
np.savez_compressed(
npz_path,
intrinsics=expected[None, ...],
)
intrinsics = self.image_depth.extract_intrinsics_array(
npz_path
)
np.testing.assert_array_equal(intrinsics, expected)
def test_camera_plane_bounds_use_intrinsics_and_median_depth(self):
source = SimpleNamespace(
data=SimpleNamespace(size=(200, 100)),
empty_image_offset=(-0.5, -0.5),
)
depth = np.full((100, 200), 2.0, dtype=np.float32)
intrinsics = np.array(
((100.0, 0.0, 100.0), (0.0, 100.0, 50.0), (0.0, 0.0, 1.0)),
dtype=np.float32,
)
bounds, reference_depth = self.image_depth.camera_plane_bounds(
source,
depth,
intrinsics,
-1.0,
)
self.assertEqual(bounds, (-2.0, 2.0, -1.0, 1.0))
self.assertEqual(reference_depth, 2.0)
def test_camera_plane_bounds_follow_displacement_unit_scale(self):
source = SimpleNamespace(
data=SimpleNamespace(size=(200, 100)),
empty_image_offset=(-0.5, -0.5),
)
depth = np.full((100, 200), 2.0, dtype=np.float32)
intrinsics = np.array(
((100.0, 0.0, 100.0), (0.0, 100.0, 50.0), (0.0, 0.0, 1.0)),
dtype=np.float32,
)
bounds, _reference_depth = (
self.image_depth.camera_plane_bounds(
source,
depth,
intrinsics,
-0.5,
)
)
self.assertEqual(bounds, (-1.0, 1.0, -0.5, 0.5))
def test_camera_plane_bounds_preserve_square_source_aspect(self):
source = SimpleNamespace(
data=SimpleNamespace(size=(447, 447)),
empty_image_offset=(-0.5, -0.5),
)
depth = np.full((140, 140), 0.7366, dtype=np.float32)
intrinsics = np.array(
(
(460.758, 0.0, 70.0),
(0.0, 393.372, 70.0),
(0.0, 0.0, 1.0),
),
dtype=np.float32,
)
bounds, _reference_depth = (
self.image_depth.camera_plane_bounds(
source,
depth,
intrinsics,
-1.0,
)
)
width = bounds[1] - bounds[0]
height = bounds[3] - bounds[2]
self.assertAlmostEqual(width / height, 1.0)
def test_raw_distance_preserves_original_values(self):
depth = np.array(
((1.25, 2.5), (5.0, np.nan)),
dtype=np.float32,
)
mapped = self.image_depth.prepare_depth_values(depth)
self.assertEqual(float(mapped[0, 0]), 1.25)
self.assertEqual(float(mapped[0, 1]), 2.5)
self.assertEqual(float(mapped[1, 0]), 5.0)
self.assertEqual(float(mapped[1, 1]), 0.0)
def test_depth_plane_uses_fixed_import_settings(self):
self.assertEqual(self.image_depth.DISPLACEMENT_STRENGTH, -1.0)
self.assertEqual(self.image_depth.DISPLACEMENT_MIDLEVEL, 1.0)
self.assertEqual(self.image_depth.BUMP_BASE_DISTANCE, 0.001)
def test_grid_segments_follow_aspect_ratio_from_subdivision_power(self):
self.assertEqual(
self.image_depth.grid_segments((0.0, 3.0, 0.0, 2.0), 1),
(3, 2),
)
self.assertEqual(
self.image_depth.grid_segments((0.0, 2.0, 0.0, 3.0), 1),
(2, 3),
)
self.assertEqual(
self.image_depth.grid_segments((0.0, 3.0, 0.0, 2.0), 3),
(12, 8),
)
def test_modifiers_use_native_names_and_fixed_levels(self):
source = inspect.getsource(self.image_depth.build_depth_plane)
self.assertIn('modifiers.new("Subdivision", "SUBSURF")', source)
self.assertIn('modifiers.new("Displace", "DISPLACE")', source)
self.assertNotIn("DA3 Subdivision", source)
self.assertNotIn("DA3 Displace", source)
self.assertIn("subdivision.levels = 1", source)
self.assertIn("subdivision.render_levels = 2", source)
def test_plane_fit_transform_matches_source_size_and_offset(self):
source = SimpleNamespace(
data=SimpleNamespace(size=(800, 400)),
empty_display_size=2.0,
empty_image_offset=(-0.25, -0.75),
)
transform = self.image_depth.plane_fit_transform(
source,
(-0.1, 0.3, -0.1, 0.1),
)
for value, expected in zip(
transform,
(5.0, 5.0, 5.0, 0.0, -0.25),
):
self.assertAlmostEqual(value, expected)
def test_source_empty_is_only_removed_when_requested(self):
source = inspect.getsource(self.image_depth.finish_depth_plane)
self.assertIn("source_name = source_object.name", source)
self.assertIn("if delete_source:", source)
self.assertIn(
"bpy.data.objects.remove(source_object, do_unlink=True)",
source,
)
self.assertNotIn("source_object.name =", source)
self.assertIn(
"bpy.data.objects.remove(source_object, do_unlink=True)\n"
" plane_object.name = source_name",
source,
)
def test_original_depth_mode_uses_uv_and_zero_midlevel(self):
source = inspect.getsource(self.image_depth.build_image_depth_plane)
self.assertIn("texture.image = source_object.data", source)
self.assertIn('modifiers.new("Displace", "DISPLACE")', source)
self.assertIn("displace.texture = texture", source)
self.assertIn('displace.texture_coords = "UV"', source)
self.assertNotIn("displace.direction", source)
self.assertNotIn("displace.strength", source)
self.assertIn(
"displace.mid_level = ORIGINAL_DISPLACEMENT_MIDLEVEL",
source,
)
self.assertIn("plane_object.matrix_world =", source)
def test_process_resolution_scales_the_image_long_edge(self):
image = SimpleNamespace(size=(1080, 1920))
self.assertEqual(
self.image_depth.scaled_process_resolution(image, 0.5),
960,
)
def test_mesh_uses_source_object_name(self):
source = inspect.getsource(self.image_depth.create_plane_mesh)
self.assertIn("bpy.data.meshes.new(source_object.name)", source)
def test_depth_data_uses_source_image_name(self):
source_object = SimpleNamespace(
name="obj_img",
data=SimpleNamespace(name="img.png"),
)
self.assertEqual(
self.image_depth.depth_data_name(source_object),
"img.depth",
)
source_object.data.name = "img.png.001"
self.assertEqual(
self.image_depth.depth_data_name(source_object),
"img.001.depth",
)
estimate_source = inspect.getsource(
self.image_depth.build_depth_plane
)
original_source = inspect.getsource(
self.image_depth.build_image_depth_plane
)
self.assertIn("depth_name = depth_data_name(source_object)", estimate_source)
self.assertIn("depth_data_name(source_object)", original_source)
def test_material_uses_source_object_name(self):
da3_source = inspect.getsource(self.image_depth.build_depth_plane)
image_source = inspect.getsource(
self.image_depth.build_image_depth_plane
)
self.assertIn(
"create_material(\n source_name,",
da3_source,
)
self.assertIn("create_material(\n source_name,", image_source)
self.assertNotIn("DA3DepthMaterial", da3_source)
def test_displace_texture_uses_extend(self):
source = inspect.getsource(
self.image_depth.build_depth_plane
)
self.assertIn("depth_name = depth_data_name(source_object)", source)
self.assertIn("create_depth_image(\n depth_name,", source)
self.assertIn("bpy.data.textures.new(\n depth_name,", source)
self.assertIn('texture.extension = "EXTEND"', source)
def test_material_supports_bump_and_optional_displacement(self):
source = inspect.getsource(self.image_depth.create_material)
self.assertIn('nodes.new("ShaderNodeBump")', source)
self.assertIn(
'color_texture.outputs["Color"], bump.inputs["Height"]',
source,
)
self.assertIn(
'bump.outputs["Normal"], shader.inputs["Normal"]',
source,
)
self.assertIn(
'bump.inputs["Distance"].default_value = '
"BUMP_BASE_DISTANCE * depth_scale",
source,
)
self.assertIn('nodes.new("ShaderNodeFloatCurve")', source)
self.assertIn(
'roughness_curve.inputs["Value"]',
source,
)
self.assertIn(
'shader.inputs["Roughness"]',
source,
)
self.assertIn(
'curve_points[0].location = (0.0, 0.5)',
source,
)
self.assertIn(
'curve_points[-1].location = (1.0, 0.1)',
source,
)
self.assertNotIn('shader.inputs["IOR"]', source)
self.assertIn(
'roughness_curve.location = (-300.0, 320.0)',
source,
)
self.assertIn('bump.location = (-300.0, -80.0)', source)
self.assertIn('nodes.new("ShaderNodeDisplacement")', source)
self.assertIn(
'depth_texture.outputs["Color"],\n'
' displacement.inputs["Height"]',
source,
)
self.assertIn(
'displacement.outputs["Displacement"],\n'
' output.inputs["Displacement"]',
source,
)
self.assertIn(
'displacement.inputs["Midlevel"].default_value',
source,
)
self.assertIn(
'displacement.inputs["Scale"].default_value',
source,
)
self.assertIn("set_displacement_only(material)", source)
def test_displacement_values_follow_depth_source(self):
estimate_source = inspect.getsource(
self.image_depth.build_depth_plane
)
original_source = inspect.getsource(
self.image_depth.build_image_depth_plane
)
self.assertIn(
"displacement_midlevel=DISPLACEMENT_MIDLEVEL",
estimate_source,
)
self.assertIn(
"displacement_scale=DISPLACEMENT_STRENGTH",
estimate_source,
)
self.assertIn(
"displacement_midlevel=ORIGINAL_DISPLACEMENT_MIDLEVEL",
original_source,
)
self.assertIn(
"displacement_scale=ORIGINAL_DISPLACEMENT_STRENGTH",
original_source,
)
self.assertEqual(self.image_depth.DISPLACEMENT_MIDLEVEL, 1.0)
self.assertEqual(self.image_depth.DISPLACEMENT_STRENGTH, -1.0)
self.assertEqual(
self.image_depth.ORIGINAL_DISPLACEMENT_MIDLEVEL,
0.0,
)
self.assertEqual(
self.image_depth.ORIGINAL_DISPLACEMENT_STRENGTH,
1.0,
)
def test_material_displacement_mode_skips_displace_modifier(self):
estimate_source = inspect.getsource(
self.image_depth.build_depth_plane
)
original_source = inspect.getsource(
self.image_depth.build_image_depth_plane
)
for source in (estimate_source, original_source):
self.assertIn('if displacement_via == "MODIFIER":', source)
self.assertIn(
'if displacement_via == "MATERIAL" else None',
source,
)
def test_surface_displacement_is_set_to_displacement_only(self):
class Material:
surface_displacement_method = "BUMP"
material = Material()
self.image_depth.set_displacement_only(material)
self.assertEqual(
material.surface_displacement_method,
"DISPLACEMENT",
)
def test_plane_transform_scales_depth_axis(self):
source = inspect.getsource(self.image_depth.apply_plane_transform)
self.assertIn("(0.0, 0.0, scale_z, 0.0)", source)
self.assertIn("plane_object.scale.z", source)
def test_plane_bounds_keep_image_aspect_and_empty_offset(self):
source = SimpleNamespace(
data=SimpleNamespace(size=(800, 400)),
empty_display_size=2.0,
empty_image_offset=(-0.5, -0.5),
)
bounds = self.image_depth.plane_bounds(source)
self.assertEqual(bounds, (-1.0, 1.0, -0.5, 0.5))
if __name__ == "__main__":
unittest.main()