Add new node asset scripts for geometry and shader operations

- Implemented `build_mesh_plane.py` to create a geometry node group for mesh planes with customizable parameters.
- Created `build_shadeless.py` for a shader node group that displays color without scene lighting while preserving shadows and reflections.
- Added `validate.py` to validate the integrity of the newly created node groups and their inputs.
- Introduced `validate_cutout.py` to ensure the functionality of cutout node groups and their dependencies.
- Developed `validate_mesh_plane.py` to test the mesh plane creation and its modifier settings.
This commit is contained in:
2026-08-30 00:17:29 +08:00
parent 7d39fe8c5e
commit 32bcd45d7b
168 changed files with 7213 additions and 13121 deletions

View File

@@ -11,10 +11,31 @@ from PIL import Image
SOURCE_ROOT = Path(__file__).parents[1] / "src"
sys.path.insert(0, str(SOURCE_ROOT))
from anyimage.operators import cutout_depth, cutout_teddy, mesh
from anyimage.operators.cutout_profile import poisson_balloon_profile
from anyimage.operators.normal_map import rasterize_tangent_normal
from anyimage.server.geometry.cutout import alpha_regions, image_alpha_regions
from anyimage.operators.cutout_tool import balloon_profile as cutout_profile
from anyimage.operators.cutout_tool import build_shapes as mesh
from anyimage.operators.cutout_tool import depth_calibration as cutout_depth
from anyimage.operators.cutout_tool import node_assets as cutout_nodes
from anyimage.operators.cutout_tool import shape_types as shapes
from anyimage.operators.cutout_tool.balloon_profile import poisson_balloon_profile
from anyimage.operators.common.normal_map import rasterize_tangent_normal
from anyimage.operators.cutout_tool.mask_regions import alpha_regions, image_alpha_regions
from anyimage.server.models import moge2 as moge2_model
def test_cutout_mesh_detail_uses_the_cropped_analysis_scale():
assert mesh.limited_cutout_mesh_detail(256, (4096, 1024), 2048) == 128
assert mesh.limited_cutout_mesh_detail(256, (1024, 512), 2048) == 256
assert mesh.limited_cutout_mesh_detail(8, (8192, 4096), 2048) == 4
def test_cutout_node_groups_are_loaded_from_the_asset():
loaded = object()
with patch.object(
cutout_nodes, "load_asset_node_group", return_value=loaded
) as load:
for shape in shapes.CUTOUT_NODE_GROUP_NAMES:
assert cutout_nodes.cutout_node_group(shape) is loaded
load.assert_called_with(shapes.CUTOUT_NODE_GROUP_NAME)
assert load.call_count == 4
def test_base_plane_fit_is_stable_for_a_slender_diagonal_domain():
@@ -40,36 +61,58 @@ def test_base_plane_fit_is_stable_for_a_slender_diagonal_domain():
def test_depth_balloon_plane_calibration_matches_cutout_axes():
vertices = np.asarray(
(
(-1.0, -1.0, 0.0),
(1.0, -1.0, 0.0),
(1.0, 1.0, 0.0),
(-1.0, 1.0, 0.0),
),
dtype=np.float32,
)
uv = np.asarray(
((0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)),
dtype=np.float32,
)
depth_data = {
"depth_signal": np.zeros((2, 2), dtype=np.float32),
"baseline_vector": np.asarray((0.4, -0.2, 0.1), dtype=np.float32),
"valid": np.ones((2, 2), dtype=bool),
"crop_size": (2, 2),
}
vertices = []
uv = []
camera_depth = np.empty((3, 3), dtype=np.float32)
for row in range(3):
local_z = 1.0 - row
for column in range(3):
local_y = column - 1.0
vertices.append((0.0, local_y, local_z))
uv.append((column * 0.5, 1.0 - row * 0.5))
camera_depth[row, column] = 2.0 + 0.2 * local_y + 0.1 * local_z
_depth, local_plane = cutout_depth.depth_balloon_fields(
reference, direction = cutout_depth.depth_balloon_projection_parameters(
vertices,
uv,
depth_data,
camera_depth,
np.ones((3, 3), dtype=bool),
0.5,
)
expected_direction = np.asarray((-0.1, 0.05, 1.0), dtype=np.float64)
expected_direction /= np.linalg.norm(expected_direction)
assert np.isclose(
reference,
np.percentile(camera_depth, 95.0) * 0.5,
atol=1e-6,
)
assert np.allclose(direction, expected_direction, atol=1e-6)
def test_depth_balloon_reference_uses_the_shared_upper_depth_percentile():
size = 10
row, column = np.indices((size, size))
camera_depth = np.full((size, size), 2.0, dtype=np.float32)
camera_depth[(row + column) % 2 == 0] = 2.2
vertices = []
uv = []
for y in range(size):
for x in range(size):
vertices.append((0.0, float(x), float(size - 1 - y)))
uv.append((x / (size - 1), 1.0 - y / (size - 1)))
reference, direction = cutout_depth.depth_balloon_projection_parameters(
vertices,
uv,
camera_depth,
np.ones((size, size), dtype=bool),
1.0,
)
oriented = cutout_depth.orient_depth_balloon_plane(local_plane)
assert np.allclose(local_plane, (0.2, 0.1, 0.05), atol=1e-6)
assert np.allclose(oriented, (-0.2, 0.1, 0.05), atol=1e-6)
assert reference > float(np.median(camera_depth))
assert np.isclose(reference, 2.2, atol=1e-6)
assert np.allclose(direction, (0.0, 0.0, 1.0), atol=1e-6)
def mesh_component_count(faces):
@@ -93,7 +136,7 @@ def mesh_component_count(faces):
return count
def test_tangent_normal_pixels_use_the_final_surface_frame():
def test_tangent_normal_pixels_use_the_final_geometry_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)),))
@@ -114,15 +157,6 @@ def test_tangent_normal_pixels_use_the_final_surface_frame():
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_depth_surface_discontinuity_map_uses_local_three_by_three_range():
depth = np.ones((5, 5), dtype=np.float32)
depth[:, 3:] = 2.0
@@ -130,8 +164,52 @@ def test_depth_surface_discontinuity_map_uses_local_three_by_three_range():
discontinuity = mesh._depth_discontinuity_map(depth, valid)
assert discontinuity[2, 0] < mesh.DEPTH_EDGE_THRESHOLD
assert discontinuity[2, 2] > mesh.DEPTH_EDGE_THRESHOLD
assert discontinuity[2, 0] == 0.0
assert discontinuity[2, 2] == 1.0
def test_depth_surface_projection_uses_one_scale_for_all_camera_axes():
points = np.zeros((51, 101, 3), dtype=np.float32)
points[..., 2] = np.linspace(1.0, 3.0, 51 * 101).reshape(51, 101)
field = (
points,
np.asarray(((100.0, 0.0, 50.0), (0.0, 100.0, 25.0), (0.0, 0.0, 1.0))),
np.ones((51, 101), dtype=bool),
np.ones((51, 101), dtype=bool),
(101, 51),
2.0,
)
uniform_scale = mesh._surface_uniform_scale(
SimpleNamespace(),
field,
bounds=(-2.0, 2.0, -1.0, 1.0),
)
assert uniform_scale == 2.0
reference_depth = cutout_depth.metric_depth_reference(
points[..., 2],
field[3],
uniform_scale,
)
assert np.isclose(reference_depth, 5.8, atol=1e-6)
def test_moge_surface_field_uses_the_model_validity_mask():
prediction = moge2_model.Prediction(
depth=np.ones((1, 2, 2), dtype=np.float32),
normal=np.zeros((1, 2, 2, 3), dtype=np.float32),
mask=np.asarray((((True, False), (True, True)),)),
intrinsics=np.asarray(
(((1.0, 0.0, 0.5), (0.0, 1.0, 0.5), (0.0, 0.0, 1.0)),),
dtype=np.float32,
),
points=np.ones((1, 2, 2, 3), dtype=np.float32),
)
frame = moge2_model.geometry_frame(prediction)
assert np.array_equal(frame.valid_mask, prediction.mask[0])
def test_cutout_regions_group_holes_with_their_connected_component():
@@ -215,7 +293,19 @@ def test_blender_cutout_alpha_pipeline_does_not_import_pillow():
assert regions
assert np.any((antialiased > 0.0) & (antialiased < 1.0))
def test_cutout_geometry_ignores_source_alpha_without_refine():
def test_unrefined_cutout_geometry_uses_strict_tenth_source_alpha_threshold():
alpha = np.full((40, 60), 0.1, dtype=np.float32)
alpha[10:30, 20:40] = 0.11
polygon = ((0, 0), (60, 0), (60, 40), (0, 40))
regions, size = mesh._geometry_cutout_regions(alpha, polygon, 64)
contour = np.asarray(regions[0][0])
assert size == (60, 40)
np.testing.assert_allclose(contour.min(axis=0), (20.0, 10.0))
np.testing.assert_allclose(contour.max(axis=0), (40.0, 30.0))
def test_cutout_geometry_excludes_zero_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
@@ -225,7 +315,7 @@ def test_cutout_geometry_ignores_source_alpha_without_refine():
assert size == (120, 80)
assert len(regions) == 1
assert len(regions[0]) == 1
assert len(regions[0]) == 2
def test_cutout_geometry_uses_final_alpha_with_refine():
@@ -242,7 +332,7 @@ def test_cutout_geometry_uses_final_alpha_with_refine():
assert len(regions) == 1
assert len(regions[0]) == 2
def test_cutout_geometry_can_extend_beyond_the_source_image():
def test_unrefined_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))
@@ -253,8 +343,8 @@ def test_cutout_geometry_can_extend_beyond_the_source_image():
[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
assert points[:, 0].min() >= 0.0
assert points[:, 0].max() <= 100.0
def test_refined_cutout_geometry_discards_out_of_frame_selection():
alpha = np.zeros((60, 100), dtype=np.float32)
@@ -297,6 +387,23 @@ def test_cutout_point_spacing_uses_the_complete_source_plane():
assert spacing == 0.02
def test_cutout_cleanup_area_uses_point_spacing_and_plane_area_scale():
assert np.isclose(
mesh._cutout_cleanup_area(
0.25,
((1.0, 0.0), (0.0, 1.0)),
),
0.0625,
)
assert np.isclose(
mesh._cutout_cleanup_area(
0.25,
((4.0, 0.0), (0.0, 9.0)),
),
0.0625 / 6.0,
)
def test_poisson_balloon_profile_is_zero_on_boundary_and_positive_inside():
points = np.asarray(
((0, 0), (2, 0), (2, 2), (0, 2), (1, 1)),
@@ -351,11 +458,11 @@ def test_teddy_balloon_profile_uses_chordal_axis_radius():
nodes = np.asarray(((1, 1), (3, 1)), dtype=float)
with patch.object(
cutout_teddy,
cutout_profile,
"chordal_axis",
return_value=(nodes, [(0, 1)], np.asarray((1.0, 1.0))),
):
profile = cutout_teddy.teddy_balloon_heights(
profile = cutout_profile.teddy_balloon_heights(
points,
faces,
points[:4],