refactor: Update cutout and depth handling by removing unused functions and adjusting parameters
This commit is contained in:
@@ -71,7 +71,7 @@ def test_camera_blur_preserves_rays_interior_protection_and_disconnected_surface
|
||||
|
||||
|
||||
|
||||
def test_cutout_fine_outline_blurs_depth_without_moving_projection():
|
||||
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
|
||||
|
||||
@@ -90,19 +90,16 @@ def test_cutout_fine_outline_blurs_depth_without_moving_projection():
|
||||
image.pixels.foreach_set(pixels.ravel())
|
||||
image.update()
|
||||
set_value("Boundary Smooth", 0)
|
||||
set_value("Smooth Weight", 0)
|
||||
set_value("Smooth Weight", 1)
|
||||
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])
|
||||
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)
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ def test_smoothing_reduces_stairs_without_leaving_two_ring_band(triangles):
|
||||
band = edge_band(faces, boundary)
|
||||
movable = np.array([i in band for i in range(len(original))])
|
||||
assert movable.any() and (~movable).any()
|
||||
set_value("Boundary Smooth", 20)
|
||||
set_value("Boundary Smooth", 16)
|
||||
smoothed, result_faces = evaluated(obj)
|
||||
assert result_faces == faces
|
||||
np.testing.assert_array_equal(smoothed[~movable], original[~movable])
|
||||
@@ -85,11 +85,11 @@ def test_smoothing_reduces_stairs_without_leaving_two_ring_band(triangles):
|
||||
np.testing.assert_array_equal(evaluated(obj)[0], original)
|
||||
|
||||
|
||||
def test_smoothing_without_split_moves_original_boundary_only_within_band():
|
||||
def test_smoothing_without_split_moves_outline_only_within_boundary_band():
|
||||
obj, set_value = diagonal_surface()
|
||||
set_value("Depth Split", 0)
|
||||
original, faces = evaluated(obj)
|
||||
set_value("Boundary Smooth", 20)
|
||||
set_value("Boundary Smooth", 16)
|
||||
result, result_faces = evaluated(obj)
|
||||
boundary = boundary_neighbors(faces)
|
||||
band = edge_band(faces, boundary)
|
||||
@@ -97,6 +97,8 @@ def test_smoothing_without_split_moves_original_boundary_only_within_band():
|
||||
assert np.max(np.linalg.norm(result[list(boundary)] - original[list(boundary)], axis=1)) > 1e-4
|
||||
np.testing.assert_array_equal(result[outside], original[outside])
|
||||
assert result_faces == faces
|
||||
set_value("Boundary Smooth", 0)
|
||||
np.testing.assert_array_equal(evaluated(obj)[0], original)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mode", [0, 1])
|
||||
@@ -147,7 +149,7 @@ def test_plane_edge_smoothing_preserves_uvs_and_closes_thickness():
|
||||
from tests.support.planes import create_surface, evaluated as plane_mesh
|
||||
|
||||
obj, set_value, _, inputs, image = create_surface("DEPTH")
|
||||
assert inputs["Boundary Smooth"].default_value == 2
|
||||
assert inputs["Boundary Smooth"].default_value == 4
|
||||
assert inputs["Boundary Smooth"].parent.name == "Options"
|
||||
pixels = np.array(image.pixels[:], np.float32).reshape(8, 16, 4)
|
||||
yy, xx = np.mgrid[:8, :16]
|
||||
|
||||
@@ -37,7 +37,7 @@ def test_boundary_triangle_cleanup_is_one_pass():
|
||||
assert all(len(f) == 3 for f in faces)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("iterations", [0, 20])
|
||||
@pytest.mark.parametrize("iterations", [0, 16])
|
||||
def test_cleaned_sawtooth_smoothing_preserves_face_orientation(iterations):
|
||||
obj, set_value = diagonal_surface(triangles=True)
|
||||
original, faces = evaluated(obj)
|
||||
|
||||
@@ -20,6 +20,36 @@ def store_test_fields(group, geometry, fields):
|
||||
return geometry
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mode", [0, 1])
|
||||
def test_normal_smooth_changes_thickness_direction_without_moving_zero_thickness(mode):
|
||||
from tests.support.depth_surface import surface, evaluated
|
||||
|
||||
obj, set_value = surface(step=1, resolution=10)
|
||||
xy = np.array([v.co[:2] for v in obj.data.vertices])
|
||||
profile = .3 * np.sin(np.pi * xy[:, 0] / 2) * np.sin(np.pi * xy[:, 1])
|
||||
profile[abs(profile) < 1e-7] = 0
|
||||
obj.data.attributes.new("o_balloon", "FLOAT", "POINT").data.foreach_set("value", profile)
|
||||
set_value("Mode", mode)
|
||||
set_value("Front Inflation", .2)
|
||||
modifier = obj.modifiers[0]
|
||||
thickness = modifier_input_identifier(modifier.node_group, "Thickness", subtype="DISTANCE" if mode else "NONE")
|
||||
|
||||
def result(amount, smoothing):
|
||||
set_modifier_input(modifier, thickness, amount)
|
||||
set_value("Normal Smooth", smoothing)
|
||||
return evaluated(obj)
|
||||
|
||||
raw, faces = result(0, 16)
|
||||
smooth, smooth_faces = result(0, 512)
|
||||
np.testing.assert_array_equal(raw, smooth)
|
||||
assert faces == smooth_faces
|
||||
raw, faces = result(1, 16)
|
||||
smooth, smooth_faces = result(1, 512)
|
||||
assert faces == smooth_faces
|
||||
assert np.isfinite(smooth).all()
|
||||
assert np.max(np.abs(raw - smooth)) > 1e-5
|
||||
|
||||
|
||||
def set_test_structure(monkeypatch, value):
|
||||
"""Control structural amplitude without depending on storage nodes."""
|
||||
from tools.nodes.groups import image_depth_cutout
|
||||
|
||||
@@ -25,7 +25,7 @@ def test_saved_parameter_descriptions_match_definitions():
|
||||
"O Image Cutout": {"Thickness", "Smooth", "Smooth Weight"},
|
||||
"O Image Depth Cutout": {
|
||||
"Thickness", "Depth Split", "Smooth", "Smooth Weight",
|
||||
"Boundary Smooth", "Outline Depth Fix", "Front Inflation",
|
||||
"Boundary Smooth", "Normal Smooth", "Front Inflation",
|
||||
},
|
||||
"O Image Depth Balloon": {"Thickness", "Smooth", "Smooth Weight"},
|
||||
"O Image Depth Panorama": {
|
||||
@@ -48,6 +48,12 @@ def test_saved_parameter_descriptions_match_definitions():
|
||||
]
|
||||
assert {item.name for item in sockets} == parameters[group.name]
|
||||
assert all(item.description for item in sockets), group.name
|
||||
if group.name in {"O Image Depth Plane", "O Image Depth Cutout"}:
|
||||
boundary = next(item for item in sockets if item.name == "Boundary Smooth")
|
||||
assert boundary.default_value == (4 if group.name == "O Image Depth Plane" else 0)
|
||||
assert boundary.min_value == 0
|
||||
assert boundary.max_value == 16
|
||||
assert boundary.parent.name == "Options"
|
||||
if group.name == "O Image Depth Panorama":
|
||||
defaults = {item.name: item.default_value for item in sockets
|
||||
if item.name in {"Subdivide", "Depth Split", "Boundary Smooth"}}
|
||||
@@ -66,8 +72,16 @@ def test_saved_parameter_descriptions_match_definitions():
|
||||
thickness = [item.description for item in sockets if item.name == "Thickness"]
|
||||
assert len(thickness) == 2 and len(set(thickness)) == 2
|
||||
if group.name == "O Image Depth Cutout":
|
||||
assert not {"Outline Depth Fix", "Center Smooth", "Center Subdivide", "Show Center"} & {
|
||||
item.name for item in group.interface.items_tree if item.item_type == "SOCKET"
|
||||
}
|
||||
inflation = next(item for item in sockets if item.name == "Front Inflation")
|
||||
assert inflation.default_value == 0.0
|
||||
assert inflation.default_value == pytest.approx(0.2)
|
||||
normal_smooth = next(item for item in sockets if item.name == "Normal Smooth")
|
||||
assert normal_smooth.default_value == 64
|
||||
assert normal_smooth.min_value == 16
|
||||
assert normal_smooth.max_value == 512
|
||||
assert normal_smooth.parent.name == "Options"
|
||||
result[group.name] = [
|
||||
(item.identifier, item.name, item.description) for item in sockets
|
||||
]
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
"""Check adaptive outline batches against a numerical graph reference."""
|
||||
|
||||
from collections import Counter
|
||||
|
||||
import bpy
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from tests.support.depth_surface import evaluated
|
||||
from tools.nodes.common.outline_smoothing import smooth_outline_camera
|
||||
from tools.nodes.common.depth_surface import CUT_ATTRIBUTE
|
||||
|
||||
|
||||
@pytest.mark.parametrize("steps", [1, 4])
|
||||
def test_outline_batches_update_weights_preserve_split_and_camera_rays(steps):
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
points = np.array([(x, y, 0) for y in range(9) for x in range(9)], dtype=float)
|
||||
faces = [(y*9+x, y*9+x+1, (y+1)*9+x+1, (y+1)*9+x) for y in range(8) for x in range(8)]
|
||||
deleted = [i % 8 == 4 for i in range(len(faces))]
|
||||
result_faces = [face for face, remove in zip(faces, deleted) if not remove]
|
||||
cut = np.isin(points[:, 0], [4, 5])
|
||||
depth = np.full(81, 2.0)
|
||||
depth[2] = 20
|
||||
depth[80] = 0
|
||||
camera = np.column_stack((points[:, :2]*depth[:, None]/10, depth))
|
||||
distance = np.linalg.norm(camera, axis=1)
|
||||
mesh = bpy.data.meshes.new("Split outline")
|
||||
mesh.from_pydata(points.tolist(), [], faces)
|
||||
for name, kind, domain, values, prop in (
|
||||
("test_camera", "FLOAT_VECTOR", "POINT", camera.ravel(), "vector"),
|
||||
(CUT_ATTRIBUTE, "FLOAT", "POINT", cut, "value"),
|
||||
("test_deleted", "BOOLEAN", "FACE", deleted, "value"),
|
||||
):
|
||||
mesh.attributes.new(name, kind, domain).data.foreach_set(prop, values)
|
||||
obj = bpy.data.objects.new("Outline", mesh)
|
||||
bpy.context.collection.objects.link(obj)
|
||||
group = bpy.data.node_groups.new("Adaptive outline test", "GeometryNodeTree")
|
||||
for direction in ("INPUT", "OUTPUT"):
|
||||
group.interface.new_socket(name="Geometry", in_out=direction, socket_type="NodeSocketGeometry")
|
||||
ids = {}
|
||||
for name in ("Outline Depth Fix", "Boundary Smooth"):
|
||||
ids[name] = group.interface.new_socket(name=name, in_out="INPUT", socket_type="NodeSocketInt").identifier
|
||||
source = group.nodes.new("NodeGroupInput")
|
||||
attributes = {}
|
||||
for name, kind in (("test_camera", "FLOAT_VECTOR"), ("test_deleted", "BOOLEAN")):
|
||||
node = group.nodes.new("GeometryNodeInputNamedAttribute")
|
||||
node.data_type = kind
|
||||
node.inputs["Name"].default_value = name
|
||||
attributes[name] = node.outputs["Attribute"]
|
||||
delete = group.nodes.new("GeometryNodeDeleteGeometry")
|
||||
delete.domain, delete.mode = "FACE", "ONLY_FACE"
|
||||
group.links.new(source.outputs["Geometry"], delete.inputs["Geometry"])
|
||||
group.links.new(attributes["test_deleted"], delete.inputs["Selection"])
|
||||
scale = group.nodes.new("ShaderNodeValue")
|
||||
scale.outputs[0].default_value = 1
|
||||
result = smooth_outline_camera(group, source.outputs["Geometry"], delete.outputs[0], attributes["test_camera"], scale.outputs[0])
|
||||
position = group.nodes.new("GeometryNodeSetPosition")
|
||||
group.links.new(delete.outputs[0], position.inputs["Geometry"])
|
||||
group.links.new(result, position.inputs["Position"])
|
||||
output = group.nodes.new("NodeGroupOutput")
|
||||
group.links.new(position.outputs[0], output.inputs["Geometry"])
|
||||
modifier = obj.modifiers.new("Outline", "NODES")
|
||||
modifier.node_group = group
|
||||
|
||||
# ONLY_FACE retains the separating column's edges in the current topology.
|
||||
edges = np.array([tuple(e.vertices) for e in mesh.edges])
|
||||
adjacency = np.zeros((81, 81))
|
||||
adjacency[edges[:, 0], edges[:, 1]] = 1
|
||||
adjacency += adjacency.T
|
||||
degree = adjacency.sum(axis=1)
|
||||
def blur(value, weight):
|
||||
return (value + weight*(adjacency @ value))/(1 + weight*degree)
|
||||
counts = Counter(tuple(sorted((a, b))) for f in result_faces for a, b in zip(f, (*f[1:], f[0])))
|
||||
boundary = np.zeros(81)
|
||||
for edge, count in counts.items():
|
||||
if count == 1:
|
||||
boundary[list(edge)] = 1
|
||||
protected = boundary.astype(bool) & cut
|
||||
seed = boundary*(~protected)
|
||||
influence = np.maximum(seed, blur(blur(seed, 1), 1))*(~protected)
|
||||
influence *= depth > 1e-8
|
||||
for rounds, batch in ((0, steps), (16, 0), (16, steps)):
|
||||
modifier[ids["Outline Depth Fix"]] = rounds
|
||||
modifier[ids["Boundary Smooth"]] = batch
|
||||
obj.update_tag(refresh={"DATA"})
|
||||
bpy.context.view_layer.update()
|
||||
actual, actual_faces = evaluated(obj)
|
||||
expected = distance.copy()
|
||||
for _ in range(rounds):
|
||||
ratio = abs(expected-blur(expected, .5))
|
||||
t = np.clip((ratio-.25)/.25, 0, 1)
|
||||
weight = t*t*(3-2*t)*influence
|
||||
for _ in range(batch):
|
||||
expected = blur(expected, weight)
|
||||
np.testing.assert_allclose(np.linalg.norm(actual, axis=1), expected, atol=2e-5)
|
||||
assert actual_faces == result_faces
|
||||
np.testing.assert_array_equal(actual[influence == 0], camera[influence == 0].astype(np.float32))
|
||||
if rounds and batch:
|
||||
assert 2 < actual[2, 2] < 15
|
||||
valid = depth > 0
|
||||
np.testing.assert_allclose(actual[valid, :2]/actual[valid, 2:3], camera[valid, :2]/camera[valid, 2:3], atol=1e-6)
|
||||
else:
|
||||
np.testing.assert_array_equal(actual, camera.astype(np.float32))
|
||||
assert not any(n.bl_idname in {"GeometryNodeCaptureAttribute", "GeometryNodeStoreNamedAttribute"} for n in group.nodes)
|
||||
Reference in New Issue
Block a user