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

@@ -136,11 +136,13 @@ class BlenderTestCase(unittest.TestCase):
)
cls.image_selection = importlib.import_module("anyimage.common.selection")
cls.image_frame = importlib.import_module(
"anyimage.operators.frame_tool"
"anyimage.operators.frame_tool.operators"
)
cls.rectify = importlib.import_module(
"anyimage.operators.rectify_tool"
"anyimage.operators.rectify_tool.operators"
)
cls.rectify_geometry = importlib.import_module("anyimage.operators.rectify_tool.geometry")
cls.rectify_preview = importlib.import_module("anyimage.operators.rectify_tool.preview")
cls.cutout_geometry = importlib.import_module(
"anyimage.operators.cutout_tool.geometry"
)
@@ -194,5 +196,3 @@ class BlenderTestCase(unittest.TestCase):
)
if hasattr(self.fake_bpy.types.Scene, "anyimage_settings"):
del self.fake_bpy.types.Scene.anyimage_settings
if hasattr(self.fake_bpy.types.Image, "anyimage_upscale_source"):
del self.fake_bpy.types.Image.anyimage_upscale_source

View File

@@ -65,7 +65,7 @@ def surface(*, step=6.0, size=1.0, resolution=2, wrapper=False):
("Depth Scale", 1.0),
("Thickness", 0.0),
("Front Inflation", 0.0),
("Split Threshold", 0.0),
("Depth Split", 0.0),
("Mode", 1),
("Smooth", 0),
("Boundary Smooth", 0),
@@ -73,7 +73,7 @@ def surface(*, step=6.0, size=1.0, resolution=2, wrapper=False):
set_value(name, value)
if wrapper:
set_value("Cleanup Threshold", 0.0)
set_value("Inward Axis", 0)
set_value("Depth Axis", 0)
set_value("Smooth", 0)
return obj, set_value

View File

@@ -35,7 +35,7 @@ def _create_cutout_shape(
depth_image=None,
depth_metadata=None,
selection_values=None,
inward_axis=cutout_object.CutoutInwardAxis.NEGATIVE_Z,
gesture="LASSO",
):
if bounds is None:
bounds = (0, 0, *tuple(int(value) for value in source.data.size))
@@ -66,7 +66,7 @@ def _create_cutout_shape(
base_shape,
depth_image=depth_image,
depth_metadata=depth_metadata,
inward_axis=inward_axis,
gesture=gesture,
color_image=color_image,
)
@@ -117,7 +117,7 @@ def _add_cutout_modifier(obj, shape):
object_data.set_modifier_input(modifier, object_data.modifier_input_identifier(group, "Mode"), 1)
object_data.set_modifier_input(
modifier,
object_data.modifier_input_identifier(group, "Inward Axis"),
object_data.modifier_input_identifier(group, "Depth Axis"),
0,
)
object_data.set_modifier_input(
@@ -171,7 +171,10 @@ def _depth_surface_step_object():
):
object_data.set_modifier_input(
modifier,
object_data.modifier_input_identifier(modifier.node_group, name),
object_data.modifier_input_identifier(
modifier.node_group, name,
subtype="DISTANCE" if name == "Thickness" else None,
),
value,
)
return obj, modifier

View File

@@ -35,11 +35,11 @@ def texture_context(node, **space_options):
edit_tree=node.id_data, id=bpy.data.materials["Texture material"], **space_options),
),
object=None,
scene=SimpleNamespace(anyimage_settings=SimpleNamespace(video_frames=16)),
scene=SimpleNamespace(anyimage_settings=SimpleNamespace()),
)
def write_result(directory, prefix="image.", count=1):
for index in range(1, count + 1):
Image.new("RGBA", (8, 6), (128, 64, 32, 80)).save(directory / f"{prefix}{index:04d}.png")
def write_result(directory, filename="foreground.png"):
path = directory / filename
Image.new("RGBA", (8, 6), (128, 64, 32, 80)).save(path)
return path

View File

@@ -9,14 +9,3 @@ CUTOUT_BUILDERS = {
"DEPTH_SOLID": build_image_depth_cutout_group,
"DEPTH_BALLOON": build_image_depth_balloon_group,
}
def validate_node_structure(node_group):
"""Check visible sockets, default titles, and paired execution endpoints."""
nodes = [node for node in node_group.nodes if node.bl_idname not in {"NodeReroute", "NodeFrame"}]
assert all(not node.label for node in nodes), node_group.name
assert not any(node.bl_idname == "FunctionNodeInputString" for node in nodes), node_group.name
assert all(socket.is_linked or socket.hide for node in node_group.nodes for socket in node.outputs), node_group.name
for node in nodes:
if node.bl_idname in {"GeometryNodeRepeatInput", "GeometryNodeSimulationInput", "GeometryNodeForeachGeometryElementInput"}:
assert node.paired_output in node_group.nodes.values(), (node_group.name, node.name)

View File

@@ -1,38 +1,29 @@
"""Evaluate complete-image projection and rectangular surface topology."""
from tests.support.paths import PROJECT_ROOT
import bpy
import numpy as np
import pytest
ROOT = PROJECT_ROOT
from tools.nodes.groups.image_plane import build_image_plane_group
from tools.nodes.groups.image_depth_plane import build_image_depth_plane_group
from tools.nodes.groups.image_relief_plane import build_image_relief_plane_group
@pytest.fixture(params=("definition", "asset"))
def surface(request):
return create_surface(request.param, "DEPTH")
@pytest.fixture
def surface():
return create_surface("DEPTH")
@pytest.fixture(params=("definition", "asset"))
def relief(request):
return create_surface(request.param, "RELIEF")
@pytest.fixture
def relief():
return create_surface("RELIEF")
def create_surface(source, plane_type):
def create_surface(plane_type):
bpy.ops.wm.read_factory_settings(use_empty=True)
group_name = f"O Image {plane_type.title()} Plane"
if source == "asset":
with bpy.data.libraries.load(str(ROOT / "src/anyimage/assets/O_AnyImage.blend")) as (library, target):
target.node_groups = [group_name]
group = target.node_groups[0]
plane = group.nodes["Group"].node_tree
else:
plane = build_image_plane_group()
builder = build_image_depth_plane_group if plane_type == "DEPTH" else build_image_relief_plane_group
group = builder(plane)
plane = build_image_plane_group()
builder = build_image_depth_plane_group if plane_type == "DEPTH" else build_image_relief_plane_group
group = builder(plane)
mesh = bpy.data.meshes.new("Rectangle")
mesh.from_pydata([(-2, -1, 0), (2, -1, 0), (2, 1, 0), (-2, 1, 0)], [], [(0, 1, 2, 3)])
material = bpy.data.materials.new("Image")
@@ -65,8 +56,8 @@ def create_surface(source, plane_type):
set_value(name, value)
if "Boundary Smooth" in inputs:
set_value("Boundary Smooth", 0)
if "Split Threshold" in inputs:
set_value("Split Threshold", 0.0)
if "Depth Split" in inputs:
set_value("Depth Split", 0.0)
return obj, set_value, plane, inputs, image