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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user