Files
AnyImage/tests/tools/nodes/test_boundary_depth.py
nan 31074bd511 Add tests and implement boundary smoothing and cutout functionality
- Introduced tests for boundary smoothing in `test_boundary_smoothing.py`, verifying that smoothing reduces stair-step artifacts while preserving boundary integrity.
- Added tests for cutout boundary behavior in `test_cutout_boundary.py`, ensuring proper handling of boundary triangles and normal blending.
- Created `test_temporary_attributes.py` to check the cleanup of temporary attributes across depth modifier branches, ensuring no warnings are raised.
- Implemented point culling functionality in `test_validity.py`, confirming that invalid points are removed without affecting adjacent geometry.
- Developed boundary smoothing logic in `boundary_smoothing.py`, allowing for depth smoothing while maintaining edge integrity.
- Added cutout boundary management in `cutout_boundary.py`, including functions for removing boundary triangles and tapering thickness profiles.
- Implemented validity checks in `validity.py` to ensure proper deletion of invalid vertices and associated wire edges.
2026-09-11 20:37:20 +08:00

187 lines
9.2 KiB
Python

"""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)
@pytest.mark.parametrize("source", ["definition", "asset"])
def test_cutout_fine_outline_blurs_depth_without_moving_projection(source):
from tests.support.depth_surface import surface
from anyimage.operators.cutout_tool.fine_mesh import build_fine_mesh
from anyimage.operators.cutout_tool.object import load_cutout_node_group
obj, set_value = surface(step=0, resolution=8)
if source == "asset":
bpy.data.node_groups.remove(obj.modifiers[0].node_group, do_unlink=True)
obj.modifiers[0].node_group = load_cutout_node_group("DEPTH_SOLID")
yy, xx = np.mgrid[:32, :64]
alpha = ((xx - 32)**2 / 27**2 + (yy - 16)**2 / 13**2 < 1).astype(float)
points, faces, _, _ = build_fine_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)
set_value("Smooth Weight", 0)
before, original_faces = evaluated(obj)
set_value("Boundary Smooth", 12)
after, result_faces = evaluated(obj)
assert result_faces == original_faces
assert after[:, 2].min() > before[:, 2].min() + 1
# Reference Depth = 1, Uniform Scale = 1 and Depth Scale = 1.
np.testing.assert_allclose(after[:, :2] / (1 - after[:, 2:3]),
before[:, :2] / (1 - before[:, 2:3]), atol=1e-6)
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])
set_value("Boundary Smooth", 0)
np.testing.assert_array_equal(evaluated(obj)[0], before)
@pytest.mark.parametrize("source", ["definition", "asset"])
def test_plane_validity_hole_blurs_depth_and_protects_rectangle(source):
from tests.support.planes import create_surface, evaluated as plane_mesh
obj, set_value, _, _, image = create_surface(source, "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), ("Smooth Weight", 0)):
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[:, 2].min() > before[:, 2].min() + .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])
np.testing.assert_allclose(after[:, :2] / (1.2 - after[:, 2:3]),
before[:, :2] / (1.2 - before[:, 2:3]), atol=1e-6)
set_value("Valid Only", False)
intact = plane_mesh(obj)[0]
set_value("Boundary Smooth", 0)
np.testing.assert_array_equal(plane_mesh(obj)[0], intact)
@pytest.mark.parametrize("source", ["definition", "asset"])
@pytest.mark.parametrize("region", ["seam", "pole"])
def test_panorama_blurs_opening_radially_and_keeps_invalid_distance(source, region):
from tests.tools.nodes.test_image_depth_panorama import panorama, evaluated as panorama_mesh
from tools.nodes.build import OUTPUT_PATH
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)
if source == "asset":
obj.modifiers[0].node_group.name = "Panorama definition"
with bpy.data.libraries.load(str(OUTPUT_PATH)) as (_, target):
target.node_groups = ["O Image Depth Panorama"]
obj.modifiers[0].node_group = target.node_groups[0]
set_value("Smooth Weight", 0)
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
np.testing.assert_allclose(after / smoothed[:, None], before / radii[:, None], atol=1e-6)
band = edge_band(faces, boundary_neighbors(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])
set_value("Valid Only", False)
near = panorama_mesh(obj)[0]
set_value("Invalid Distance", 250)
far = panorama_mesh(obj)[0]
valid = np.linalg.norm(near, axis=1) < 50
assert valid.any() and (~valid).any()
np.testing.assert_array_equal(far[valid], near[valid])
np.testing.assert_allclose(np.linalg.norm(far[~valid], axis=1), 250, atol=5e-5)
set_value("Depth Scale", 0)
np.testing.assert_allclose(np.linalg.norm(panorama_mesh(obj)[0], axis=1), 1, atol=1e-6)