Add new functionality for image rectification and depth metadata handling

- Implemented a preview tool for rectifying images with interactive aspect controls in `preview.py`.
- Added tests for depth metadata loading to ensure consistent schema usage in `test_depth_metadata.py`.
- Developed comprehensive tests for cutout calibration processes in `test_cutout_calibration.py`, including stability checks and depth calibration.
- Created tests for frame compositing operations in `test_frame_compositing.py`, ensuring correct handling of depth and transparency.
- Introduced material slot inheritance functionality in `material_slots.py` to manage geometry material assignments.
- Built shared shader nodes for material processing in `shader.py`, including roughness and bump mapping setups.
This commit is contained in:
2026-09-12 19:20:06 +08:00
parent e8268504c2
commit 938d95db0f
175 changed files with 3812 additions and 6864 deletions

View File

@@ -21,44 +21,12 @@ from anyimage.server.geometry.depth_texture import (
write_depth_texture,
)
from anyimage.server.models.geometry import GeometryFrame
from tests.support.image_objects import _add_cutout_modifier, _create_cutout_shape, _depth_image, _depth_metadata, _depth_surface_step_object, _evaluated_positions, _evaluated_topology, _group_node, _inputs
def test_object_normal_image_follows_cutout_inward_axis():
bpy.ops.wm.read_factory_settings(use_empty=True)
image = bpy.data.images.new(
"Object Normal",
width=1,
height=1,
alpha=True,
float_buffer=True,
)
decoded = np.asarray((0.2, -0.4, 0.8), dtype=np.float32)
encoded = decoded * 0.5 + 0.5
image.pixels.foreach_set((*encoded, 0.3))
cutout_object.orient_object_normal_image(
image,
cutout_object.CutoutInwardAxis.NEGATIVE_Z,
)
actual = np.empty(4, dtype=np.float32)
image.pixels.foreach_get(actual)
assert np.allclose(actual[:3], encoded, atol=1e-6)
assert np.isclose(actual[3], 0.3)
cutout_object.orient_object_normal_image(
image,
cutout_object.CutoutInwardAxis.POSITIVE_X,
)
image.pixels.foreach_get(actual)
expected = np.asarray((-0.8, -0.2, -0.4), dtype=np.float32) * 0.5 + 0.5
assert np.allclose(actual[:3], expected, atol=1e-6)
assert np.isclose(actual[3], 0.3)
from tests.support.image_objects import _add_cutout_modifier, _create_cutout_shape, _depth_image, _depth_metadata, _depth_surface_step_object, _evaluated_positions, _evaluated_topology, _group_node
@pytest.mark.parametrize("shape", ("FLAT", "SOLID"))
def test_cutout_axes_produce_the_same_world_geometry(shape):
@pytest.mark.parametrize("gesture", ("LASSO", "POLYLINE"))
def test_cutout_depth_axis_is_adjusted_only_in_modifier(shape, gesture):
bpy.ops.wm.read_factory_settings(use_empty=True)
image = bpy.data.images.new("Axis Source", width=32, height=16, alpha=True)
source = bpy.data.objects.new("Axis Source", None)
@@ -76,22 +44,38 @@ def test_cutout_axes_produce_the_same_world_geometry(shape):
source,
shape,
8,
inward_axis=cutout_object.CutoutInwardAxis.POSITIVE_X,
gesture=gesture,
)
negative_z = _create_cutout_shape(
source,
shape,
8,
inward_axis=cutout_object.CutoutInwardAxis.NEGATIVE_Z,
gesture=gesture,
)
for result in (positive_x, negative_z):
modifier = result.modifiers[0]
mode = object_data.modifier_input_identifier(modifier.node_group, "Mode")
assert modifier[mode] == (1 if gesture == "POLYLINE" else 0)
thickness = object_data.modifier_input_identifier(
modifier.node_group, "Thickness", subtype="DISTANCE",
)
assert modifier[thickness] == (0.5 if shape == "SOLID" and gesture == "POLYLINE" else 0.0)
local = np.asarray([tuple(vertex.co) for vertex in result.data.vertices])
assert np.allclose(local[:, 2], 0.0, atol=1e-8)
assert np.ptp(local[:, 0]) > 0.0
assert np.ptp(local[:, 1]) > 0.0
assert all(polygon.normal.z > 0.99 for polygon in result.data.polygons)
assert np.allclose(positive_x.matrix_world, source.matrix_world)
modifier = positive_x.modifiers[0]
axis = object_data.modifier_input_identifier(modifier.node_group, "Depth Axis")
assert modifier[axis] == 0
object_data.set_modifier_input(modifier, axis, 1)
positive_x.update_tag(refresh={"DATA"})
bpy.context.view_layer.update()
positive_x.matrix_world @= Matrix(cutout_shape.CUTOUT_Z_TO_X).inverted()
def world_positions(obj):
local = _evaluated_positions(obj)
return np.asarray([tuple(obj.matrix_world @ Vector(point)) for point in local])
@@ -165,37 +149,6 @@ def test_depth_texture_keeps_points_outside_positive_validity(tmp_path):
assert np.array_equal(packed[..., 3], frame.validity)
def test_cutout_presets_load_three_direct_modifier_assets():
bpy.ops.wm.read_factory_settings(use_empty=True)
groups = {shape: cutout_object.load_cutout_node_group(shape) for shape in cutout_shape.CUTOUT_SHAPES}
assert groups["FLAT"] == groups["SOLID"]
assert len(set(groups.values())) == 3
for shape, group in groups.items():
assert group.name == cutout_shape.CUTOUT_NODE_GROUP_NAMES[shape]
assert group.is_modifier
assert "Shape" not in _inputs(group)
assert not any(node.bl_idname == "GeometryNodeGroup" for node in group.nodes)
assert all(socket.is_linked or socket.hide for node in group.nodes for socket in node.outputs)
for shape in ("SOLID", "DEPTH_SOLID"):
group = groups[shape]
inputs = {item.name: item for item in group.interface.items_tree if item.item_type == "SOCKET" and item.in_out == "INPUT"}
assert next(s for s in group.interface.items_tree if s.name == "Thickness").default_value == 1
thickness = [item for item in group.interface.items_tree if item.name == "Thickness"]
assert [item.subtype for item in thickness] == ["NONE", "DISTANCE"]
assert inputs["Thickness"].subtype == "DISTANCE"
assert inputs["Mode"].socket_type == "NodeSocketMenu"
assert inputs["Mode"].default_value == "Balloon"
assert inputs["Smooth"].default_value == 0
assert inputs["Smooth Weight"].default_value == 1
if shape == "DEPTH_SOLID":
assert inputs["Boundary Smooth"].default_value == 2
assert inputs["Boundary Smooth"].parent.name == "Options"
else:
assert "Boundary Smooth" not in inputs
assert not any(node.bl_idname == "GeometryNodeRepeatInput" and node.inputs["Iterations"].default_value == 8 for node in group.nodes)
assert next(item for item in groups["DEPTH_BALLOON"].interface.items_tree if item.name == "Double Sided").default_value
def test_balloon_build_starts_from_the_unified_single_sheet():
bpy.ops.wm.read_factory_settings(use_empty=True)
image = bpy.data.images.new("Balloon Sheet", width=64, height=64, alpha=True)
@@ -271,7 +224,8 @@ def test_teddy_preference_writes_the_shared_balloon_attribute():
)
def test_depth_surface_protected_smooth_evaluates():
@pytest.mark.parametrize("gesture", ("LASSO", "POLYLINE"))
def test_depth_surface_protected_smooth_evaluates(gesture):
bpy.ops.wm.read_factory_settings(use_empty=True)
image = bpy.data.images.new("Surface", width=64, height=64)
source = bpy.data.objects.new("Surface", None)
@@ -300,12 +254,21 @@ def test_depth_surface_protected_smooth_evaluates():
8,
depth_image=depth_image,
depth_metadata=depth_metadata,
gesture=gesture,
)
assert all(abs(vertex.co.z) < 1e-8 for vertex in result.data.vertices)
assert cutout_shape.BALLOON_ATTRIBUTE_NAME in result.data.attributes
modifier = result.modifiers[0]
assert modifier.node_group.name == cutout_shape.CUTOUT_NODE_GROUP_NAMES["DEPTH_SOLID"]
assert modifier[object_data.modifier_input_identifier(modifier.node_group, "Mode")] == (1 if gesture == "POLYLINE" else 0)
assert modifier[object_data.modifier_input_identifier(modifier.node_group, "Thickness", subtype="DISTANCE")] == 0.5
assert modifier[object_data.modifier_input_identifier(modifier.node_group, "Thickness", subtype="NONE")] == 1.0
object_data.set_modifier_input(
modifier,
object_data.modifier_input_identifier(modifier.node_group, "Thickness", subtype="DISTANCE"),
0.0,
)
expected_cleanup = cutout_geometry.cutout_cleanup_value(
cutout_geometry.cutout_sampling_spacing(source, 8),
cutout_geometry.world_plane_metric(source.matrix_world)[0],
@@ -431,7 +394,7 @@ def test_depth_surface_keeps_continuous_points_across_zero_validity_edge():
def test_depth_surface_removes_boundary_ears_without_loose_geometry():
bpy.ops.wm.read_factory_settings(use_empty=True)
obj, modifier = _depth_surface_step_object()
split_id = object_data.modifier_input_identifier(modifier.node_group, "Split Threshold")
split_id = object_data.modifier_input_identifier(modifier.node_group, "Depth Split")
# Both outer ears have three boundary vertices, including the raised corner.
for value in (0., .5, 1.):
object_data.set_modifier_input(modifier, split_id, value)
@@ -473,8 +436,8 @@ def test_cutout_thickness_keeps_one_connected_shell():
assert len(result.vertices) == 18
assert len(result.polygons) == 24
z = np.asarray([vertex.co.z for vertex in result.vertices])
assert np.isclose(z.min(), -0.5, atol=1e-6)
assert np.isclose(z.max(), 0.5, atol=1e-6)
assert np.isclose(z.min(), -1.0, atol=1e-6)
assert np.isclose(z.max(), 0.0, atol=1e-6)
front_faces = [
polygon for polygon in result.polygons if polygon.normal.z > 0.9
]
@@ -623,7 +586,7 @@ def test_cutout_cleanup_uses_absolute_area_with_many_small_islands():
assert len(_evaluated_positions(obj)) == 9
def test_depth_balloon_uses_metric_plane_inputs_without_controller():
def test_depth_balloon_uses_metric_plane_inputs():
bpy.ops.wm.read_factory_settings(use_empty=True)
image = bpy.data.images.new("Depth Balloon", width=100, height=100, alpha=True)
source = bpy.data.objects.new("Depth Balloon", None)
@@ -667,9 +630,6 @@ def test_depth_balloon_uses_metric_plane_inputs_without_controller():
assert all(abs(vertex.co.z) < 1e-8 for vertex in result.data.vertices)
assert cutout_shape.BALLOON_ATTRIBUTE_NAME in result.data.attributes
assert "o_balloon_depth" not in result.data.attributes
assert "o_depth_offset" not in result.data.attributes
assert not result.children
assert {obj.name for obj in bpy.context.scene.objects} == {
source.name,
result.name,