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

@@ -24,7 +24,7 @@ def diagonal_surface(triangles=False):
pixels[..., 2] = 1 + (xx > 18 + yy * 0.7) * 6
image.pixels.foreach_set(pixels.ravel())
image.update()
set_value("Split Threshold", 0.5)
set_value("Depth Split", 0.5)
set_value("Depth Scale", 0)
return obj, set_value
@@ -85,16 +85,9 @@ def test_smoothing_reduces_stairs_without_leaving_two_ring_band(triangles):
np.testing.assert_array_equal(evaluated(obj)[0], original)
@pytest.mark.parametrize("source", ["definition", "asset"])
def test_smoothing_without_split_moves_original_boundary_only_within_band(source):
def test_smoothing_without_split_moves_original_boundary_only_within_band():
obj, set_value = diagonal_surface()
if source == "asset":
from anyimage.common.node import node_asset_path
with bpy.data.libraries.load(str(node_asset_path())) as (_, target):
target.node_groups = ["O Image Depth Cutout"]
obj.modifiers[0].node_group = target.node_groups[0]
set_value("Split Threshold", 0)
set_value("Depth Split", 0)
original, faces = evaluated(obj)
set_value("Boundary Smooth", 20)
result, result_faces = evaluated(obj)
@@ -136,7 +129,7 @@ def test_panorama_smoothing_preserves_interior_and_uvs():
yy, xx = np.mgrid[:64, :128]
depth = np.where(xx > 35 + yy * 0.6, 8.0, 2.0)
obj, set_value = panorama(depth, subdivide=4)
set_value("Split Threshold", 0.5)
set_value("Depth Split", 0.5)
original, faces, uv = panorama_mesh(obj)
rim = boundary_neighbors(faces)
band = edge_band(faces, rim)
@@ -150,11 +143,10 @@ def test_panorama_smoothing_preserves_interior_and_uvs():
assert np.max(np.linalg.norm(result-original, axis=1)) > 1e-4
@pytest.mark.parametrize("source", ["definition", "asset"])
def test_plane_edge_smoothing_preserves_uvs_and_closes_thickness(source):
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(source, "DEPTH")
obj, set_value, _, inputs, image = create_surface("DEPTH")
assert inputs["Boundary Smooth"].default_value == 2
assert inputs["Boundary Smooth"].parent.name == "Options"
pixels = np.array(image.pixels[:], np.float32).reshape(8, 16, 4)
@@ -164,7 +156,7 @@ def test_plane_edge_smoothing_preserves_uvs_and_closes_thickness(source):
image.update()
set_value("Subdivide", 5)
set_value("Depth Scale", 1)
set_value("Split Threshold", 0.5)
set_value("Depth Split", 0.5)
original, faces, uv, *_ = plane_mesh(obj)
set_value("Boundary Smooth", 4)
result, result_faces, result_uv, *_ = plane_mesh(obj)
@@ -175,76 +167,13 @@ def test_plane_edge_smoothing_preserves_uvs_and_closes_thickness(source):
assert_closed(plane_mesh(obj)[1])
def test_edge_iterations_reuse_boundary_topology_and_mapping():
from tools.nodes.arrangement.flow import origin_socket
from tools.nodes.arrangement.zones import node_zones
obj, _ = diagonal_surface()
group = obj.modifiers[0].node_group
entry, _, members = next(
zone for zone in node_zones(group)
if origin_socket(zone[0].inputs["Iterations"].links[0].from_socket).name == "Boundary Smooth"
)
nearest = next(node for node in group.nodes if node.bl_idname == "GeometryNodeSampleNearest"
and any(link.to_node in members for link in node.outputs["Index"].links))
fixed_position = origin_socket(nearest.inputs["Sample Position"].links[0].from_socket).node
assert fixed_position.bl_idname == "GeometryNodeSampleIndex"
source = origin_socket(entry.inputs["Geometry"].links[0].from_socket)
assert origin_socket(fixed_position.inputs["Geometry"].links[0].from_socket) == source
assert fixed_position.domain == "POINT"
assert any(node.bl_idname == "GeometryNodeSeparateGeometry" for node in members)
@pytest.mark.parametrize("source", ["definition", "asset"])
def test_every_split_node_enables_boundary_smoothing_by_default(source):
from tools.nodes.build import OUTPUT_PATH, build_node_groups
from tools.nodes.arrangement.flow import origin_socket
bpy.ops.wm.read_factory_settings(use_empty=True)
if source == "definition":
groups = build_node_groups()
else:
with bpy.data.libraries.load(str(OUTPUT_PATH)) as (library, target):
target.node_groups = list(library.node_groups)
groups = target.node_groups
split_groups = []
for group in groups:
inputs = {item.name: item for item in group.interface.items_tree
if item.item_type == "SOCKET" and item.in_out == "INPUT"}
assert "Edge Smooth" not in inputs
if "Split Threshold" not in inputs:
continue
split_groups.append(group.name)
smooth = inputs["Boundary Smooth"]
assert smooth.default_value == (5 if group.name == "O Image Depth Panorama" else 2)
assert (smooth.min_value, smooth.max_value) == (1 if "Outline Depth Fix" in inputs else 0, 20)
assert smooth.parent.name == "Options"
assert not {"Depth Blur Iterations", "Depth Band Rings", "Depth Blur Strength", "Skip Inactive Geometry"} & inputs.keys()
depth_blurs = [n for n in group.nodes if n.bl_idname == "GeometryNodeBlurAttribute"
and n.data_type == "FLOAT" and n.inputs["Iterations"].is_linked
and origin_socket(n.inputs["Iterations"].links[0].from_socket).name == "Boundary Smooth"]
assert len(depth_blurs) == 1
if "Outline Depth Fix" in inputs:
assert inputs["Outline Depth Fix"].default_value == 16
assert origin_socket(depth_blurs[0].inputs["Weight"].links[0].from_socket).node.bl_idname == "GeometryNodeSampleIndex"
continue
depth_bypass = [n for n in group.nodes if n.bl_idname == "GeometryNodeSwitch"
and n.input_type == "FLOAT" and n.inputs["Switch"].is_linked
and origin_socket(n.inputs["Switch"].links[0].from_socket).name == "Boundary Smooth"]
assert len(depth_bypass) == 1
assert set(split_groups) == {
"O Image Depth Plane", "O Image Depth Cutout", "O Image Depth Panorama",
}
@pytest.mark.parametrize("source", ["definition", "asset"])
def test_validity_cut_smoothing_preserves_original_outline_and_interior(source):
def test_validity_cut_smoothing_preserves_original_outline_and_interior():
from tests.support.planes import create_surface, evaluated as plane_mesh
obj, set_value, _, _, image = create_surface(source, "DEPTH")
obj, set_value, _, _, image = create_surface("DEPTH")
set_value("Subdivide", 5)
set_value("Split Threshold", 0)
set_value("Valid Only", False)
set_value("Depth Split", 0)
set_value("Depth Mask", False)
whole = plane_mesh(obj)[0]
low, high = whole.min(axis=0), whole.max(axis=0)
pixels = np.array(image.pixels[:], np.float32).reshape(8, 16, 4)
@@ -252,7 +181,7 @@ def test_validity_cut_smoothing_preserves_original_outline_and_interior(source):
pixels[..., 3] = xx < 5 + yy * 0.7
image.pixels.foreach_set(pixels.ravel())
image.update()
set_value("Valid Only", True)
set_value("Depth Mask", True)
before, faces, uv, *_ = plane_mesh(obj)
boundary = boundary_neighbors(faces)
outer = np.any(np.isclose(before[:, :2], low[:2]) | np.isclose(before[:, :2], high[:2]), axis=1)
@@ -274,7 +203,7 @@ def test_validity_cut_smoothing_preserves_original_outline_and_interior(source):
set_value("Thickness", 0.03)
assert_closed(plane_mesh(obj)[1])
set_value("Thickness", 0)
set_value("Valid Only", False)
set_value("Depth Mask", False)
np.testing.assert_array_equal(plane_mesh(obj)[0], whole)
@@ -284,7 +213,7 @@ def test_panorama_validity_cut_smoothing_stays_in_two_ring_band():
yy, xx = np.mgrid[:64, :128]
alpha = ((xx-64)**2 + (yy-32)**2 > 15**2).astype(np.float32)
obj, set_value = panorama(np.full((64, 128), 3), alpha, subdivide=4)
set_value("Split Threshold", 0)
set_value("Depth Split", 0)
before, faces, uv = panorama_mesh(obj)
band = edge_band(faces, boundary_neighbors(faces))
outside = [i for i in range(len(before)) if i not in band]
@@ -294,45 +223,3 @@ def test_panorama_validity_cut_smoothing_stays_in_two_ring_band():
np.testing.assert_array_equal(result_uv, uv)
np.testing.assert_array_equal(after[outside], before[outside])
assert np.max(np.linalg.norm(after-before, axis=1)) > 1e-4
@pytest.mark.parametrize("source", ["definition", "asset"])
@pytest.mark.parametrize("kind", ["cutout", "plane", "panorama"])
@pytest.mark.parametrize("iterations,weight", [(0, 1.0), (8, 1.0), (8, 0.0)])
def test_boundary_loop_bypass_uses_iterations_only(source, kind, iterations, weight):
from anyimage.common.node import node_asset_path
from tests.support.planes import create_surface
from tests.tools.nodes.test_image_depth_panorama import panorama
from tools.nodes.arrangement.flow import origin_socket
if kind == "cutout":
obj, set_value = surface(resolution=4)
elif kind == "plane":
obj, set_value, *_ = create_surface("definition", "DEPTH")
else:
obj, set_value = panorama(subdivide=2)
if source == "asset":
with bpy.data.libraries.load(str(node_asset_path())) as (_, target):
target.node_groups = [obj.modifiers[0].node_group.name]
obj.modifiers[0].node_group = target.node_groups[0].copy()
set_value("Split Threshold", 0)
set_value("Smooth Weight", weight)
set_value("Boundary Smooth", iterations)
group = obj.modifiers[0].node_group
entry = next(n for n in group.nodes if n.bl_idname == "GeometryNodeRepeatInput"
and origin_socket(n.inputs["Iterations"].links[0].from_socket).name == "Boundary Smooth")
bypass = next(n for n in group.nodes if n.bl_idname == "GeometryNodeSwitch"
and n.input_type == "GEOMETRY" and n.inputs["True"].is_linked
and origin_socket(n.inputs["True"].links[0].from_socket).node == entry.paired_output)
control = origin_socket(bypass.inputs["Switch"].links[0].from_socket)
assert control.name == "Boundary Smooth"
assert control.identifier == origin_socket(entry.inputs["Iterations"].links[0].from_socket).identifier
assert origin_socket(bypass.inputs["False"].links[0].from_socket) == origin_socket(entry.inputs["Geometry"].links[0].from_socket)
original, faces = evaluated(obj)
group.links.remove(bypass.inputs["Switch"].links[0])
bypass.inputs["Switch"].default_value = True
obj.update_tag(refresh={"DATA"})
bpy.context.view_layer.update()
unskipped, unskipped_faces = evaluated(obj)
np.testing.assert_array_equal(original, unskipped)
assert faces == unskipped_faces