import builtins import sys import tempfile from pathlib import Path from types import SimpleNamespace from unittest.mock import patch import numpy as np from PIL import Image SOURCE_ROOT = Path(__file__).parents[1] / "src" sys.path.insert(0, str(SOURCE_ROOT)) from anyimage.operators import mesh from anyimage.operators.normal_map import rasterize_tangent_normal from anyimage.server.geometry.cutout import alpha_regions, image_alpha_regions def mesh_component_count(faces): adjacency = {} for face in faces: for vertex in face: adjacency.setdefault(int(vertex), set()) for start, end in zip(face, np.roll(face, -1)): adjacency[int(start)].add(int(end)) adjacency[int(end)].add(int(start)) remaining = set(adjacency) count = 0 while remaining: count += 1 pending = [remaining.pop()] while pending: vertex = pending.pop() connected = adjacency[vertex] & remaining remaining.difference_update(connected) pending.extend(connected) return count def test_tangent_normal_pixels_use_the_final_surface_frame(): object_normal = np.zeros((3, 3, 3), dtype=np.float32) object_normal[..., 2] = 1.0 triangle_uv = np.asarray((((0.0, 0.0), (1.0, 0.0), (0.0, 1.0)),)) triangle_normal = np.asarray((((1.0, 0.0, 0.0),) * 3,)) triangle_tangent = np.asarray((((0.0, 1.0, 0.0),) * 3,)) triangle_sign = np.ones((1, 3), dtype=np.float32) tangent = rasterize_tangent_normal( object_normal, triangle_uv, triangle_normal, triangle_tangent, triangle_sign, ) assert np.allclose(tangent[0, 0], (0.0, 1.0, 0.0)) assert not np.allclose(tangent[0, 0], object_normal[0, 0]) assert np.allclose(tangent[2, 2], (0.0, 0.0, 1.0)) def test_plane_source_topology_is_one_quad_at_every_detail_level(): low = mesh._plane_domain((-2.0, 2.0, -1.0, 1.0), 1) high = mesh._plane_domain((-2.0, 2.0, -1.0, 1.0), 512) assert low[0].shape == high[0].shape == (4, 3) assert low[1].shape == high[1].shape == (1, 4) def test_cutout_front_back_and_side_faces_point_outward(): vertices = np.asarray( ((0.0, 0.0, 0.0), (0.0, 1.0, 0.0), (1.0, 0.0, 0.0)), dtype=np.float64, ) faces = np.asarray(((0, 1, 2),), dtype=np.int32) uv = vertices[:, :2].astype(np.float32) boundary = np.ones(3, dtype=bool) layered = mesh._layered_geometry( (vertices, faces, uv, boundary, vertices[:, :2]), mesh.CUTOUT, ) positioned = layered[0] + layered[3] output_faces = layered[1] def face_normal(face): points = positioned[np.asarray(face[:3], dtype=np.int32)] return np.cross(points[1] - points[0], points[2] - points[0]) assert face_normal(output_faces[0])[2] > 0.0 assert face_normal(output_faces[1])[2] < 0.0 center = vertices[:, :2].mean(axis=0) for face in output_faces[2:]: normal = face_normal(face) side_center = positioned[np.asarray(face, dtype=np.int32), :2].mean(axis=0) assert np.dot(normal[:2], side_center - center) > 0.0 def test_depth_surface_moves_both_sides_with_camera_projection_and_extrudes_back(): domain = mesh._plane_domain((-1.0, 1.0, -1.0, 1.0), 4) field = np.zeros((2, 2, 3), dtype=np.float32) field[..., 2] = 1.0 valid = np.ones((2, 2), dtype=bool) camera_depth = np.ones((2, 2), dtype=np.float32) depth_surface = mesh._layered_geometry( domain, mesh.DEPTH_SURFACE, (field, valid, camera_depth) ) base_count = len(domain[0]) assert np.allclose(depth_surface[4][:base_count, 2], 1.0) assert np.allclose(depth_surface[4][base_count:, 2], 1.0) assert np.allclose(depth_surface[3][:base_count], 0.0) assert np.allclose(depth_surface[3][base_count:, 2], -1.0) assert all(depth_surface[5][: len(domain[1])]) assert not any(depth_surface[5][len(domain[1]) :]) def test_depth_surface_records_depth_discontinuities_for_geometry_nodes(): faces = np.asarray(((0, 1, 2), (1, 2, 3)), dtype=np.int32) depth = np.asarray((1.0, 1.01, 1.02, 2.0), dtype=np.float32) discontinuity = mesh._depth_discontinuity_values(faces, depth) assert discontinuity[0] < mesh.DEPTH_EDGE_THRESHOLD assert discontinuity[1] > mesh.DEPTH_EDGE_THRESHOLD def test_depth_surface_discontinuity_map_uses_local_three_by_three_range(): depth = np.ones((5, 5), dtype=np.float32) depth[:, 3:] = 2.0 valid = np.ones_like(depth, dtype=bool) discontinuity = mesh._depth_discontinuity_map(depth, valid) assert discontinuity[2, 0] < mesh.DEPTH_EDGE_THRESHOLD assert discontinuity[2, 2] > mesh.DEPTH_EDGE_THRESHOLD def test_cutout_regions_group_holes_with_their_connected_component(): with tempfile.TemporaryDirectory() as directory: path = Path(directory) / "cutout.png" pixels = np.zeros((120, 200, 4), dtype=np.uint8) pixels[20:100, 50:160] = (20, 40, 60, 255) pixels[45:75, 80:120] = 0 pixels[5:15, 5:15] = (20, 40, 60, 255) Image.fromarray(pixels, mode="RGBA").save(path) regions, size = image_alpha_regions(path, max_resolution=200) assert size == (200, 120) assert sorted(len(region) for region in regions) == [1, 2] contours = [contour for region in regions for contour in region] bounds = [ (*np.min(contour, axis=0), *np.max(contour, axis=0)) for contour in contours ] assert (50.0, 20.0, 160.0, 100.0) in bounds assert (80.0, 45.0, 120.0, 75.0) in bounds assert (5.0, 5.0, 15.0, 15.0) in bounds def test_alpha_regions_accepts_blender_float_alpha_with_a_hole(): alpha = np.zeros((120, 200), dtype=np.float32) alpha[20:100, 50:160] = 1.0 alpha[45:75, 80:120] = 0.0 regions, size = alpha_regions(alpha, max_resolution=200) assert size == (200, 120) assert len(regions) == 1 assert len(regions[0]) == 2 def test_cutout_crop_bounds_map_to_the_source_plane(): source = SimpleNamespace( data=SimpleNamespace(size=(200, 100)), empty_display_size=2.0, empty_image_offset=(-0.5, -0.5), ) bounds = mesh._crop_plane_bounds( source, (50, 25, 150, 75), (200, 100), ) np.testing.assert_allclose(bounds, (-0.5, 0.5, -0.25, 0.25)) def test_depth_surface_cutout_uses_half_alpha_boundary(): alpha = np.zeros((12, 16), dtype=np.float32) alpha[2:10, 2:14] = 0.49 alpha[4:8, 5:11] = 0.51 regions, size = mesh._cutout_regions(alpha, 64, threshold=0.5) contour = np.asarray(regions[0][0]) assert size == (16, 12) np.testing.assert_allclose(contour.min(axis=0), (5.0, 4.0)) np.testing.assert_allclose(contour.max(axis=0), (11.0, 8.0)) def test_blender_cutout_alpha_pipeline_does_not_import_pillow(): original_import = builtins.__import__ def without_pillow(name, *args, **kwargs): if name == "PIL" or name.startswith("PIL."): raise ModuleNotFoundError("Pillow is unavailable in Blender") return original_import(name, *args, **kwargs) selection = np.zeros((32, 48), dtype=bool) selection[4:28, 6:42] = True with patch("builtins.__import__", side_effect=without_pillow): antialiased = mesh._antialias_selection(selection) regions, size = alpha_regions(antialiased, max_resolution=24) assert size == (48, 32) assert regions assert np.any((antialiased > 0.0) & (antialiased < 1.0)) def test_cutout_geometry_ignores_source_alpha_without_refine(): alpha = np.zeros((80, 120), dtype=np.float32) alpha[10:70, 10:110] = 1.0 alpha[30:50, 45:75] = 0.0 polygon = ((10, 10), (110, 10), (110, 70), (10, 70)) regions, size = mesh._geometry_cutout_regions(alpha, polygon, 64) assert size == (120, 80) assert len(regions) == 1 assert len(regions[0]) == 1 def test_cutout_geometry_uses_final_alpha_with_refine(): alpha = np.zeros((80, 120), dtype=np.float32) alpha[10:70, 10:110] = 1.0 alpha[30:50, 45:75] = 0.0 polygon = ((10, 10), (110, 10), (110, 70), (10, 70)) regions, size = mesh._geometry_cutout_regions( alpha, polygon, 64, refine_selection=True ) assert size == (120, 80) assert len(regions) == 1 assert len(regions[0]) == 2 def test_cutout_geometry_can_extend_beyond_the_source_image(): alpha = np.zeros((60, 100), dtype=np.float32) alpha[10:50] = 1.0 polygon = ((-20, 10), (120, 10), (120, 50), (-20, 50)) regions, size = mesh._geometry_cutout_regions(alpha, polygon, 64) points = np.concatenate( [np.asarray(contour) for region in regions for contour in region] ) assert size == (100, 60) assert points[:, 0].min() <= -19.0 assert points[:, 0].max() >= 119.0 def test_refined_cutout_geometry_discards_out_of_frame_selection(): alpha = np.zeros((60, 100), dtype=np.float32) alpha[10:50] = 1.0 polygon = ((-20, 10), (120, 10), (120, 50), (-20, 50)) regions, size = mesh._geometry_cutout_regions( alpha, polygon, 64, refine_selection=True ) points = np.concatenate( [np.asarray(contour) for region in regions for contour in region] ) assert size == (100, 60) assert points[:, 0].min() >= 0.0 assert points[:, 0].max() <= 100.0 def test_blender_cutout_contour_resolution_tracks_mesh_detail(): expected = ([(((0, 0), (1, 0), (0, 1)),)], (1, 1)) alpha = np.ones((2, 2), dtype=np.float32) with patch.object(mesh, "alpha_regions", return_value=expected) as trace: assert mesh._cutout_regions(alpha, 32) == expected assert mesh._cutout_regions(alpha, 256) == expected assert trace.call_args_list[0].kwargs["max_resolution"] == 512 assert trace.call_args_list[1].kwargs["max_resolution"] == 1024 def test_cutout_point_spacing_uses_the_complete_source_plane(): source = SimpleNamespace(matrix_world=object()) with ( patch.object(mesh, "plane_bounds", return_value=(-2.0, 2.0, -1.0, 1.0)), patch.object( mesh, "_world_plane_metric", return_value=(((1.0, 0.0), (0.0, 1.0)), 1.0), ), ): spacing = mesh._cutout_point_spacing(source, 200) assert spacing == 0.02 def test_cutout_domain_adapts_boundary_spacing_for_a_small_valid_region(): regions = ( (((2046, 2046), (2050, 2046), (2050, 2050), (2046, 2050)),), ) domain = mesh._cutout_domain( regions, (4096, 4096), (-1.0, 1.0, -1.0, 1.0), 0.5, ((1.0, 0.0), (0.0, 1.0)), ) assert len(domain[0]) >= 3 assert len(domain[1]) >= 1 def test_cutout_domain_builds_a_full_image_at_minimum_detail(): regions = ((((0, 0), (4096, 0), (4096, 4096), (0, 4096)),),) domain = mesh._cutout_domain( regions, (4096, 4096), (-1.0, 1.0, -1.0, 1.0), 0.5, ((1.0, 0.0), (0.0, 1.0)), ) assert len(domain[0]) >= 4 assert len(domain[1]) >= 2 def test_cutout_domain_keeps_overlapping_regions_disconnected(): regions = ( ( ((20, 0), (100, 0), (100, 80), (20, 80)), ((50, 20), (50, 60), (70, 60), (70, 20)), ), ( ((0, 20), (40, 20), (40, 50), (0, 50)), ), ) domain = mesh._cutout_domain( regions, (120, 80), (0.0, 1.0, 0.0, 1.0), 0.025, ((1.0, 0.0), (0.0, 1.0)), ) centers = domain[2][domain[1]].mean(axis=1) in_hole = ( (centers[:, 0] > 0.45) & (centers[:, 0] < 0.55) & (centers[:, 1] > 0.35) & (centers[:, 1] < 0.65) ) in_left_region = ( (centers[:, 0] > 0.02) & (centers[:, 0] < 0.12) & (centers[:, 1] > 0.4) & (centers[:, 1] < 0.75) ) assert not in_hole.any() assert in_left_region.any() assert mesh_component_count(domain[1]) == 2