Refactor depth-related tests and improve parameter handling in image cutout symmetry
This commit is contained in:
@@ -137,7 +137,7 @@ def test_plane_validity_hole_blurs_depth_and_protects_rectangle():
|
||||
|
||||
|
||||
@pytest.mark.parametrize("region", ["seam", "pole"])
|
||||
def test_panorama_blurs_opening_radially_and_keeps_invalid_distance(region):
|
||||
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]
|
||||
@@ -162,21 +162,3 @@ def test_panorama_blurs_opening_radially_and_keeps_invalid_distance(region):
|
||||
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])
|
||||
set_value("Depth Mask", False)
|
||||
near = panorama_mesh(obj)[0]
|
||||
set_value("Invalid Distance", 250)
|
||||
far, far_faces, _ = panorama_mesh(obj)
|
||||
valid = np.linalg.norm(near, axis=1) < 50
|
||||
assert valid.any() and (~valid).any()
|
||||
np.testing.assert_array_equal(far[valid], near[valid])
|
||||
# The smoothed rim blends into its neighbours, so only the interior keeps the exact distance.
|
||||
far_band = edge_band(far_faces, boundary_neighbors(far_faces))
|
||||
kept = [i for i in range(len(far)) if i not in far_band and not valid[i]]
|
||||
assert kept
|
||||
np.testing.assert_allclose(np.linalg.norm(far[kept], axis=1), 250, atol=5e-5)
|
||||
set_value("Depth Scale", 0)
|
||||
sphere, sphere_faces, _ = panorama_mesh(obj)
|
||||
sphere_band = edge_band(sphere_faces, boundary_neighbors(sphere_faces))
|
||||
interior = [i for i in range(len(sphere)) if i not in sphere_band]
|
||||
assert interior
|
||||
np.testing.assert_allclose(np.linalg.norm(sphere[interior], axis=1), 1, atol=1e-6)
|
||||
|
||||
@@ -16,7 +16,6 @@ def pinned_group(
|
||||
iterations=1,
|
||||
region=1.0,
|
||||
weight=1.0,
|
||||
movement=None,
|
||||
pin_boundary=True,
|
||||
pin_sharp=True,
|
||||
):
|
||||
@@ -39,7 +38,6 @@ def pinned_group(
|
||||
control.outputs["Smooth"],
|
||||
region_value.outputs[0],
|
||||
weight=weight,
|
||||
movement=movement,
|
||||
pin_boundary=pin_boundary,
|
||||
pin_sharp=pin_sharp,
|
||||
)
|
||||
@@ -396,16 +394,3 @@ def test_junction_band_keeps_the_sharpness_weight():
|
||||
assert flat_step > 1e-3
|
||||
assert creased_step < flat_step * 0.99
|
||||
|
||||
|
||||
def test_movement_mask_keeps_the_symmetry_plane():
|
||||
"""Restricting the movement leaves the masked axis untouched."""
|
||||
points, faces = grid(4)
|
||||
# 网格落在 XZ 对称平面上,Y 是只允许被松开的对称轴。
|
||||
points = [(x, z, y) for x, y, z in points]
|
||||
points[5] = (2.0, 0.5, 1.0)
|
||||
original = np.array(points)
|
||||
masked = pinned_result(points, faces, movement=(1, 0, 1))
|
||||
np.testing.assert_array_equal(masked[:, 1], original[:, 1])
|
||||
assert np.max(np.abs(masked[:, 0] - original[:, 0])) > 1e-4
|
||||
full = pinned_result(points, faces)
|
||||
assert np.max(np.abs(full[:, 1] - original[:, 1])) > 1e-4
|
||||
|
||||
@@ -9,7 +9,7 @@ from scipy.spatial import cKDTree
|
||||
|
||||
from anyimage.common.object import modifier_input_identifier, set_modifier_input
|
||||
from anyimage.operators.cutout_tool.object import load_cutout_node_group
|
||||
from tests.support.depth_surface import assert_closed, assert_single_vertex_fans, evaluated
|
||||
from tests.support.depth_surface import assert_closed, evaluated
|
||||
from tools.nodes.common.normal_map import AXIS_ATTRIBUTE, FACE_ATTRIBUTE, ROTATION_ATTRIBUTE
|
||||
from tools.nodes.groups.image_depth_cutout import build_image_depth_cutout_group
|
||||
from tools.nodes.groups.image_cutout_symmetry import build_image_cutout_symmetry_group
|
||||
@@ -87,7 +87,7 @@ def symmetry(request):
|
||||
|
||||
cutout_setter = _set_value(obj, cutout, cutout_group)
|
||||
mirror_setter = _set_value(obj, mirror, mirror_group)
|
||||
mirror_setter("Fill Smooth", 0)
|
||||
mirror_setter("Smooth", 0)
|
||||
for name, value in (
|
||||
("Depth Image", image),
|
||||
("Reference Depth", 1.45),
|
||||
@@ -112,8 +112,7 @@ def test_symmetry_interface_and_cutout_defaults(symmetry):
|
||||
"Offset",
|
||||
"Scale",
|
||||
"Fill Sides",
|
||||
"Fill Smooth",
|
||||
"Seam Smooth",
|
||||
"Smooth",
|
||||
]
|
||||
assert tuple(mirror_inputs[1].default_value) == (0, 0, 1)
|
||||
assert mirror_inputs[3].default_value == 1
|
||||
@@ -122,7 +121,7 @@ def test_symmetry_interface_and_cutout_defaults(symmetry):
|
||||
assert mirror_inputs[3].min_value == 0
|
||||
assert mirror_inputs[3].max_value == 2
|
||||
assert mirror_inputs[4].default_value is True
|
||||
assert mirror_inputs[5].default_value == 2
|
||||
assert mirror_inputs[5].default_value == 4
|
||||
|
||||
cutout_names = {
|
||||
item.name
|
||||
@@ -148,7 +147,6 @@ def test_final_symmetry_axis_is_x_and_z_is_up(symmetry):
|
||||
assert cKDTree(points).query(points * (-1, 1, 1))[0].max() < 1e-5
|
||||
assert np.ptp(points[:, 2]) > 1.0
|
||||
assert_closed(faces)
|
||||
assert_single_vertex_fans(faces)
|
||||
|
||||
|
||||
def _tolerance(points):
|
||||
@@ -204,7 +202,7 @@ def test_seam_clamps_geometry_onto_the_symmetry_plane(symmetry):
|
||||
assert _marked_vertices(points, faces, marker, 2)[:, 0].max() <= tolerance
|
||||
assert plane.any()
|
||||
assert not any(all(plane[index] for index in face) for face in faces)
|
||||
# Kept straddling faces fold onto the plane instead of being discarded.
|
||||
# 跨界的面保留,切口环被回拉到对称面上。
|
||||
assert any(plane[list(face)].any() and not plane[list(face)].all() for face in faces)
|
||||
|
||||
|
||||
@@ -223,9 +221,9 @@ def test_disabled_fill_keeps_the_welded_seam(symmetry):
|
||||
def test_fill_smooth_relaxes_the_junction_band(symmetry):
|
||||
obj, _cutout, _mirror, _cutout_setter, mirror_setter = symmetry
|
||||
mirror_setter("Fill Sides", True)
|
||||
mirror_setter("Fill Smooth", 0)
|
||||
mirror_setter("Smooth", 0)
|
||||
plain_points, plain_faces, plain_marker = _evaluated(obj)
|
||||
mirror_setter("Fill Smooth", 4)
|
||||
mirror_setter("Smooth", 4)
|
||||
smooth_points, smooth_faces, smooth_marker = _evaluated(obj)
|
||||
tolerance = _tolerance(plain_points)
|
||||
|
||||
@@ -233,23 +231,22 @@ def test_fill_smooth_relaxes_the_junction_band(symmetry):
|
||||
plane = np.abs(points[:, 0]) <= tolerance
|
||||
assert plane.any()
|
||||
assert not any(all(plane[index] for index in face) for face in faces)
|
||||
assert smooth_faces == plain_faces
|
||||
axis = np.abs(plain_points[:, 0]) <= tolerance
|
||||
moved = np.linalg.norm(smooth_points - plain_points, axis=1)
|
||||
# 衔接带同时覆盖对称轴上的点与外侧轮廓。
|
||||
assert moved[axis].max() > 1e-4
|
||||
plain_rim = _positions(_front_rim(plain_points, plain_faces, plain_marker, tolerance))
|
||||
smooth_rim = _positions(_front_rim(smooth_points, smooth_faces, smooth_marker, tolerance))
|
||||
assert plain_rim != smooth_rim
|
||||
assert_closed(faces)
|
||||
# 焊好之后衔接带连同补面一起松弛,贴合对称面的一圈随之变形。
|
||||
band = 0.25 * np.ptp(plain_points[:, 0])
|
||||
plain_band = _positions(plain_points[np.abs(plain_points[:, 0]) <= band])
|
||||
smooth_band = _positions(smooth_points[np.abs(smooth_points[:, 0]) <= band])
|
||||
assert plain_band != smooth_band
|
||||
assert cKDTree(smooth_points).query(smooth_points * (-1, 1, 1))[0].max() < 1e-5
|
||||
|
||||
|
||||
def test_fill_smooth_band_reaches_beyond_two_rings():
|
||||
"""Fill Smooth 平滑的是衔接带,覆盖范围比原先的两圈更宽。"""
|
||||
"""Smooth 平滑的是衔接带,覆盖范围比原先的两圈更宽。"""
|
||||
from tools.nodes.common.depth_surface import _math
|
||||
from tools.nodes.groups.image_cutout_symmetry import FILL_SMOOTH_RINGS, _falloff
|
||||
from tools.nodes.groups.image_cutout_symmetry import SMOOTH_RINGS, _falloff
|
||||
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
size, rows = FILL_SMOOTH_RINGS + 4, 3
|
||||
size, rows = SMOOTH_RINGS + 4, 3
|
||||
points = [(x, 0.0, y) for y in range(rows) for x in range(size)]
|
||||
faces = [
|
||||
(
|
||||
@@ -279,7 +276,7 @@ def test_fill_smooth_band_reaches_beyond_two_rings():
|
||||
store.data_type, store.domain = "FLOAT", "POINT"
|
||||
store.inputs["Name"].default_value = "fill_band"
|
||||
group.links.new(source.outputs["Geometry"], store.inputs["Geometry"])
|
||||
group.links.new(_falloff(group, outline, FILL_SMOOTH_RINGS), store.inputs["Value"])
|
||||
group.links.new(_falloff(group, outline, SMOOTH_RINGS), store.inputs["Value"])
|
||||
group.links.new(
|
||||
store.outputs["Geometry"], group.nodes.new("NodeGroupOutput").inputs["Geometry"]
|
||||
)
|
||||
@@ -293,7 +290,7 @@ def test_fill_smooth_band_reaches_beyond_two_rings():
|
||||
finally:
|
||||
result.to_mesh_clear()
|
||||
assert (band[:, 3] > 0).all()
|
||||
assert (band[:, FILL_SMOOTH_RINGS + 1] == 0).all()
|
||||
assert (band[:, SMOOTH_RINGS + 1] == 0).all()
|
||||
|
||||
|
||||
def test_object_normal_attributes_follow_final_orientation(symmetry):
|
||||
|
||||
@@ -49,12 +49,15 @@ def evaluated(obj):
|
||||
result.to_mesh_clear()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dome", [1.0, 50.0])
|
||||
@pytest.mark.parametrize("subdivide", [0, 4])
|
||||
def test_quad_sphere_topology_and_radial_endpoints(subdivide):
|
||||
def test_quad_sphere_topology_and_radial_endpoints(subdivide, dome):
|
||||
obj, set_value = panorama(subdivide=subdivide)
|
||||
set_value("Dome Radius", dome)
|
||||
inputs = [s for s in obj.modifiers[0].node_group.interface.items_tree if s.item_type == "SOCKET" and s.in_out == "INPUT"]
|
||||
assert [s.name for s in inputs if s.socket_type == "NodeSocketImage"] == ["Depth Image"]
|
||||
for scale, radius in ((0, 1), (0.5, 2), (1, 3)):
|
||||
# Depth Scale blends between the dome sphere and the estimated distance.
|
||||
for scale, radius in ((0, dome), (0.5, dome + 0.5 * (3 - dome)), (1, 3)):
|
||||
set_value("Depth Scale", scale)
|
||||
points, faces, uv = evaluated(obj)
|
||||
assert len(faces) == 6 * 4 ** subdivide
|
||||
@@ -68,7 +71,7 @@ def test_quad_sphere_topology_and_radial_endpoints(subdivide):
|
||||
assert np.max(np.ptp(uv.reshape(-1, 4, 2)[..., 0], axis=1)) <= 0.5 + 1e-5
|
||||
|
||||
|
||||
def test_depth_alpha_culls_faces_without_double_multiplication():
|
||||
def test_mask_threshold_culls_faces():
|
||||
obj, set_value = panorama(alpha=0.5)
|
||||
assert not len(evaluated(obj)[0])
|
||||
set_value("Depth Mask", False)
|
||||
@@ -112,39 +115,45 @@ def test_linear_depth_reconstructs_flat_floor_without_sampling_rings():
|
||||
assert np.max(abs(error)) < 0.004
|
||||
|
||||
|
||||
@pytest.mark.parametrize("sky_alpha", [0, 1])
|
||||
def test_invalid_distance_removes_faces_between_valid_and_invalid_regions(sky_alpha):
|
||||
depth = np.full((64, 128), 3.0)
|
||||
depth[:, 64:] = 1e-7
|
||||
def test_masked_area_falls_back_to_the_dome_radius():
|
||||
"""The mask decides validity; what Depth Mask keeps lands on the dome sphere."""
|
||||
alpha = np.ones((64, 128))
|
||||
alpha[:, 64:] = sky_alpha
|
||||
alpha[:, 64:] = 0
|
||||
depth = np.where(alpha, 3.0, 1e-7)
|
||||
obj, set_value = panorama(depth, alpha, subdivide=4)
|
||||
points, original_faces, _ = evaluated(obj)
|
||||
assert np.linalg.norm(points, axis=1).min() > 1
|
||||
assert np.linalg.norm(points, axis=1).max() < 3.01
|
||||
set_value("Invalid Distance", 250)
|
||||
assert evaluated(obj)[1] == original_faces
|
||||
points, faces, _ = evaluated(obj)
|
||||
# Depth Mask removes the masked half; the cut ring keeps its face depth.
|
||||
assert len(faces) == 768
|
||||
np.testing.assert_allclose(np.linalg.norm(points, axis=1), 3, atol=1e-3)
|
||||
set_value("Depth Mask", False)
|
||||
for background in (100, 250):
|
||||
set_value("Invalid Distance", background)
|
||||
points, faces, _ = evaluated(obj)
|
||||
radii = np.linalg.norm(points, axis=1)
|
||||
assert np.any(np.isclose(radii, background))
|
||||
assert np.any(np.isclose(radii, 3))
|
||||
assert len(original_faces) < len(faces) < 6 * 4 ** 4
|
||||
assert all(np.all(radii[list(face)] > 10) or np.all(radii[list(face)] < 10) for face in faces)
|
||||
for dome in (10.0, 50.0):
|
||||
set_value("Dome Radius", dome)
|
||||
radii = np.linalg.norm(evaluated(obj)[0], axis=1)
|
||||
on_dome = np.isclose(radii, dome, atol=1e-3)
|
||||
assert on_dome.any() and np.isclose(radii, 3, atol=1e-3).any()
|
||||
assert np.all(on_dome | np.isclose(radii, 3, atol=1e-3))
|
||||
set_value("Dome Radius", 50.0)
|
||||
set_value("Depth Scale", 0)
|
||||
np.testing.assert_allclose(np.linalg.norm(evaluated(obj)[0], axis=1), 1, atol=1e-5)
|
||||
np.testing.assert_allclose(np.linalg.norm(evaluated(obj)[0], axis=1), 50, atol=1e-3)
|
||||
set_value("Depth Mask", True)
|
||||
assert len(evaluated(obj)[1]) == 768
|
||||
np.testing.assert_allclose(np.linalg.norm(evaluated(obj)[0], axis=1), 50, atol=1e-3)
|
||||
|
||||
|
||||
def test_controls_and_material_slot_inheritance():
|
||||
obj, set_value = panorama()
|
||||
group = obj.modifiers[0].node_group
|
||||
inputs = {s.name: s for s in group.interface.items_tree if s.item_type == "SOCKET" and s.in_out == "INPUT"}
|
||||
names = [item.name for item in group.interface.items_tree
|
||||
if item.item_type == "SOCKET" and item.in_out == "INPUT"]
|
||||
assert names[names.index("Subdivide"):names.index("Depth Scale") + 1] == [
|
||||
"Subdivide", "Dome Radius", "Depth Scale",
|
||||
]
|
||||
assert inputs["Dome Radius"].parent == inputs["Subdivide"].parent
|
||||
assert inputs["Dome Radius"].default_value == 50
|
||||
assert (inputs["Dome Radius"].min_value, inputs["Dome Radius"].max_value) == (1, 100)
|
||||
assert inputs["Dome Radius"].subtype == "DISTANCE"
|
||||
assert inputs["Mask Threshold"].parent.name == "Options"
|
||||
assert inputs["Invalid Distance"].parent.name == "Options"
|
||||
assert inputs["Invalid Distance"].default_value == 100
|
||||
assert inputs["Invalid Distance"].subtype == "DISTANCE"
|
||||
assert inputs["Boundary Smooth"].parent.name == "Options"
|
||||
assert "Smooth Weight" not in inputs
|
||||
assert inputs["Depth Mask"].parent == inputs["Subdivide"].parent
|
||||
|
||||
@@ -30,11 +30,11 @@ def test_saved_parameter_descriptions_match_definitions():
|
||||
"Boundary Smooth", "Normal Smooth", "Front Inflation",
|
||||
},
|
||||
"O Image Cutout Symmetry": {
|
||||
"Direction", "Offset", "Scale", "Fill Sides", "Fill Smooth",
|
||||
"Direction", "Offset", "Scale", "Fill Sides", "Smooth",
|
||||
},
|
||||
"O Image Depth Panorama": {
|
||||
"Subdivide", "Depth Scale", "Depth Split",
|
||||
"Depth Mask", "Mask Threshold", "Boundary Smooth", "Invalid Distance",
|
||||
"Subdivide", "Depth Scale", "Depth Split", "Dome Radius",
|
||||
"Depth Mask", "Mask Threshold", "Boundary Smooth",
|
||||
},
|
||||
"O Image Layer": {"Alpha Fix", "Normal Scale", "Bump Scale", "Object Space"},
|
||||
"O Image Depth Layer": {"Bump Scale"},
|
||||
@@ -100,6 +100,9 @@ def test_saved_parameter_descriptions_match_definitions():
|
||||
})
|
||||
boundary = next(item for item in input_items if item.name == "Boundary Smooth")
|
||||
assert (boundary.min_value, boundary.max_value) == (0, 16)
|
||||
dome = next(item for item in input_items if item.name == "Dome Radius")
|
||||
assert (dome.min_value, dome.max_value) == (1, 100)
|
||||
assert dome.default_value == 50
|
||||
if group.name == "O Image Relief Plane":
|
||||
names = [item.name for item in group.interface.items_tree
|
||||
if item.item_type == "SOCKET" and item.in_out == "INPUT"]
|
||||
@@ -149,10 +152,10 @@ def test_saved_parameter_descriptions_match_definitions():
|
||||
if group.name == "O Image Cutout Symmetry":
|
||||
offset = next(item for item in input_items if item.name == "Offset")
|
||||
scale = next(item for item in input_items if item.name == "Scale")
|
||||
fill_smooth = next(item for item in input_items if item.name == "Fill Smooth")
|
||||
smooth = next(item for item in input_items if item.name == "Smooth")
|
||||
assert (offset.min_value, offset.max_value) == (-10.0, 10.0)
|
||||
assert (scale.min_value, scale.max_value) == (0.0, 2.0)
|
||||
assert (fill_smooth.min_value, fill_smooth.max_value) == (0, 16)
|
||||
assert (smooth.min_value, smooth.max_value) == (0, 16)
|
||||
result[group.name] = [
|
||||
(item.identifier, item.name, item.description) for item in sockets
|
||||
]
|
||||
@@ -212,6 +215,7 @@ def test_geometry_attribute_contract_includes_nested_groups(source):
|
||||
"UVMap": ("FLOAT2", "CORNER"),
|
||||
"o_balloon": ("FLOAT", "POINT"),
|
||||
"_o_anyimage_depth_cut": ("BOOLEAN", "CORNER"),
|
||||
"_o_anyimage_symmetry_weld": ("BOOLEAN", "POINT"),
|
||||
"o_depth_rotation": ("FLOAT_VECTOR", "FACE"),
|
||||
"o_depth_face": ("FLOAT", "FACE"),
|
||||
"o_depth_axis": ("FLOAT", "FACE"),
|
||||
|
||||
Reference in New Issue
Block a user