"""Verify boundary depth filtering before camera and radial projection.""" import bpy import numpy as np import pytest from tests.support.depth_surface import evaluated from tests.tools.nodes.test_boundary_smoothing import boundary_neighbors, edge_band from tools.nodes.common.boundary_smoothing import smooth_boundary_camera def test_camera_blur_preserves_rays_interior_protection_and_disconnected_surfaces(): bpy.ops.wm.read_factory_settings(use_empty=True) points, faces, cameras, protected = [], [], [], [] for offset, depth in ((0, 2), (81, 50)): for y in range(9): for x in range(9): value = 20 if offset == 0 and (x, y) == (4, 0) else depth if offset == 0 and (x, y) == (8, 8): value = 0 points.append((x, y, 0)) cameras.append((x * value / 10, y * value / 10, value)) protected.append(x == 0) faces.extend((offset + y * 9 + x, offset + y * 9 + x + 1, offset + (y + 1) * 9 + x + 1, offset + (y + 1) * 9 + x) for y in range(8) for x in range(8)) mesh = bpy.data.meshes.new("Disconnected camera samples") mesh.from_pydata(points, [], faces) camera = np.asarray(cameras, np.float32) mesh.attributes.new("test_camera", "FLOAT_VECTOR", "POINT").data.foreach_set("vector", camera.ravel()) mesh.attributes.new("test_protected", "BOOLEAN", "POINT").data.foreach_set("value", protected) obj = bpy.data.objects.new("Depth samples", mesh) bpy.context.collection.objects.link(obj) group = bpy.data.node_groups.new("Boundary depth test", "GeometryNodeTree") for direction in ("INPUT", "OUTPUT"): group.interface.new_socket(name="Geometry", in_out=direction, socket_type="NodeSocketGeometry") control = group.interface.new_socket(name="Boundary Smooth", in_out="INPUT", socket_type="NodeSocketInt") source = group.nodes.new("NodeGroupInput") attributes = [] for name, kind in (("test_camera", "FLOAT_VECTOR"), ("test_protected", "BOOLEAN")): node = group.nodes.new("GeometryNodeInputNamedAttribute") node.data_type = kind node.inputs["Name"].default_value = name attributes.append(node.outputs["Attribute"]) position = group.nodes.new("GeometryNodeSetPosition") group.links.new(source.outputs["Geometry"], position.inputs["Geometry"]) group.links.new(smooth_boundary_camera(group, *attributes), position.inputs["Position"]) output = group.nodes.new("NodeGroupOutput") group.links.new(position.outputs[0], output.inputs["Geometry"]) modifier = obj.modifiers.new("Depth", "NODES") modifier.node_group = group modifier[control.identifier] = 0 np.testing.assert_array_equal(evaluated(obj)[0], camera) modifier[control.identifier] = 12 obj.update_tag(refresh={"DATA"}) bpy.context.view_layer.update() result, result_faces = evaluated(obj) assert result_faces == faces assert 2 < result[4, 2] < 10 assert np.isfinite(result).all() np.testing.assert_array_equal(result[protected], camera[protected]) np.testing.assert_array_equal(result[81:], camera[81:]) np.testing.assert_array_equal(result[80], camera[80]) band = edge_band(faces, set(boundary_neighbors(faces)) - set(np.flatnonzero(protected))) interior = [i for i in range(len(camera)) if i not in band] assert interior np.testing.assert_array_equal(result[interior], camera[interior]) valid = camera[:, 2] > 0 np.testing.assert_allclose(result[valid, :2] / result[valid, 2:3], camera[valid, :2] / camera[valid, 2:3], atol=1e-6) def test_cutout_fine_outline_smooths_only_within_boundary_band(): from tests.support.depth_surface import surface from anyimage.operators.cutout_tool.mesh import build_mesh obj, set_value = surface(step=0, resolution=8) yy, xx = np.mgrid[:32, :64] alpha = ((xx - 32)**2 / 27**2 + (yy - 16)**2 / 13**2 < 1).astype(float) points, faces, _, _, _ = build_mesh(alpha, 3, .9) obj.data.clear_geometry() obj.data.from_pydata([(x / 32, y / 32, 0) for x, y in points], [], faces.tolist()) uv = obj.data.uv_layers.get("UVMap") or obj.data.uv_layers.new(name="UVMap") for loop in obj.data.loops: uv.data[loop.index].uv = points[loop.vertex_index] / (64, 32) image = bpy.data.images["Camera"] pixels = np.array(image.pixels[:], np.float32).reshape(32, 64, 4) pixels[..., 2] = 2 + 12 * ((xx > 25) & (xx < 39) & (yy > 25)) image.pixels.foreach_set(pixels.ravel()) image.update() set_value("Boundary Smooth", 0) before, original_faces = evaluated(obj) set_value("Boundary Smooth", 12) after, result_faces = evaluated(obj) assert result_faces == original_faces band = edge_band(original_faces, boundary_neighbors(original_faces)) interior = [i for i in range(len(before)) if i not in band] assert interior np.testing.assert_array_equal(after[interior], before[interior]) assert np.max(np.linalg.norm(after - before, axis=1)) > 1e-4 set_value("Boundary Smooth", 0) np.testing.assert_array_equal(evaluated(obj)[0], before) def test_plane_validity_hole_blurs_depth_and_protects_rectangle(): from tests.support.planes import create_surface, evaluated as plane_mesh obj, set_value, _, _, image = create_surface("DEPTH") pixels = np.array(image.pixels[:], np.float32).reshape(8, 16, 4) pixels[..., 2] = 2 pixels[2:6, 6:10, 3] = 0 pixels[1:3, 7:9, 2] = 12 image.pixels.foreach_set(pixels.ravel()) image.update() for name, value in (("Subdivide", 5), ("Depth Scale", 1)): set_value(name, value) before, faces, uv, *_ = plane_mesh(obj) set_value("Boundary Smooth", 12) after, result_faces, result_uv, *_ = plane_mesh(obj) assert faces == result_faces np.testing.assert_array_equal(result_uv, uv) assert after[:, 1].max() < before[:, 1].max() - .1 protected = np.any(np.isclose(uv, 0) | np.isclose(uv, 1), axis=1) corners = np.concatenate(faces) outer = np.unique(corners[protected]) np.testing.assert_array_equal(after[outer], before[outer]) # Position smoothing moves the cut band off its projection ray by design. band = edge_band(faces, boundary_neighbors(faces)) kept = [i for i in range(len(before)) if i not in band] np.testing.assert_allclose(after[kept][:, [0, 2]] / (1.2 + after[kept][:, 1:2]), before[kept][:, [0, 2]] / (1.2 + before[kept][:, 1:2]), atol=1e-6) set_value("Depth Mask", False) intact = plane_mesh(obj)[0] set_value("Boundary Smooth", 0) np.testing.assert_array_equal(plane_mesh(obj)[0], intact) @pytest.mark.parametrize("region", ["seam", "pole"]) def test_panorama_blurs_opening_radially(region): from tests.tools.nodes.test_image_depth_panorama import panorama, evaluated as panorama_mesh yy, xx = np.mgrid[:64, :128] distance = np.minimum(xx, 128 - xx) hole = (distance**2 + (yy - 32)**2 < 9**2) if region == "seam" else (yy > 54) spike = ((distance < 4) & (yy > 40) & (yy < 44)) if region == "seam" else ((yy > 50) & (xx < 8)) depth = np.where(spike, 10., 3.) obj, set_value = panorama(depth, (~hole).astype(float), subdivide=5) before, faces, uv = panorama_mesh(obj) set_value("Boundary Smooth", 12) after, result_faces, result_uv = panorama_mesh(obj) assert result_faces == faces np.testing.assert_array_equal(result_uv, uv) radii = np.linalg.norm(before, axis=1) smoothed = np.linalg.norm(after, axis=1) rim = list(boundary_neighbors(faces)) assert smoothed[rim].max() < radii[rim].max() - .1 band = edge_band(faces, boundary_neighbors(faces)) interior = [i for i in range(len(before)) if i not in band] assert interior # Position smoothing moves the opening rim off its radial direction by design. np.testing.assert_allclose(after[interior] / smoothed[interior, None], before[interior] / radii[interior, None], atol=1e-6) np.testing.assert_array_equal(after[interior], before[interior])