Files
AnyImage/tools/validate_moge_surface.py
nan 2b79a342b0 Add comprehensive tests for cutout and mesh functionalities
- Introduced `test_cutout.py` to validate cutout pie menu behavior, image lasso options, and mesh detail calculations.
- Added `test_mesh.py` to ensure correct tangent normal rasterization, plane source topology, and depth surface evaluations.
- Created `test_mesh_blender.py` to verify material node groups, tangent baking, and local cutout building from Blender images.
- Implemented tests for handling alpha regions and ensuring geometry nodes maintain expected behavior.
2026-08-26 10:42:29 +08:00

299 lines
11 KiB
Python

import sys
import tempfile
from pathlib import Path
import bpy
import numpy as np
PROJECT_ROOT = Path(__file__).parents[1]
sys.path.insert(0, str(PROJECT_ROOT / "src"))
sys.path.insert(
0,
str(next((PROJECT_ROOT / "src" / "anyimage" / "wheels").glob("blendjob-*.whl"))),
)
from anyimage.operators.material import (
MOGE_SURFACE_MATERIAL_NODE_GROUP_NAME,
material_color_space_name,
)
from anyimage.operators.surface import MOGE_NODE_GROUP_NAME, build_image_mesh
from anyimage.server.geometry import GeometryFrame, build_surface
from anyimage.server.outputs.surface import write_surface_archive
def write_test_normal_texture(normal, output_path):
converted = normal * np.array([1.0, -1.0, -1.0], dtype=np.float32)
rgb = np.clip((converted + 1.0) * 0.5, 0.0, 1.0)
alpha = np.ones((*rgb.shape[:2], 1), dtype=np.float32)
pixels = np.concatenate((rgb, alpha), axis=-1)
image = bpy.data.images.new(
"MoGe Test Normal",
width=rgb.shape[1],
height=rgb.shape[0],
alpha=True,
float_buffer=False,
)
image.pixels.foreach_set(pixels.reshape(-1))
image.file_format = "PNG"
image.filepath_raw = str(output_path)
image.save()
return output_path
def main():
bpy.ops.wm.read_factory_settings(use_empty=True)
arguments = sys.argv[sys.argv.index("--") + 1 :] if "--" in sys.argv else []
if arguments:
validate_real_result(Path(arguments[0]), Path(arguments[1]))
return
height, width = 48, 64
u, v = np.meshgrid(
np.linspace(-0.5, 0.5, width, dtype=np.float32),
np.linspace(-0.375, 0.375, height, dtype=np.float32),
)
depth = np.full((1, height, width), 2.0, dtype=np.float32)
points = np.stack((u * 2.0, v * 2.0, depth[0]), axis=-1)[None]
mask = np.ones((1, height, width), dtype=bool)
mask[:, :2] = False
intrinsics = np.array(
[[[1.0, 0.0, 0.5], [0.0, 1.0, 0.5], [0.0, 0.0, 1.0]]],
dtype=np.float32,
)
image = bpy.data.images.new("MoGe Surface Test", width=width, height=height)
foreground = bpy.data.images.new(
"MoGe Surface Test.color",
width=width,
height=height,
)
source = bpy.data.objects.new("MoGe Surface Test", None)
source.empty_display_type = "IMAGE"
source.data = image
bpy.context.collection.objects.link(source)
with tempfile.TemporaryDirectory() as temporary:
directory = Path(temporary)
mesh_path = directory / "mesh.npz"
normal = np.broadcast_to(
np.array([0.0, 0.0, -1.0], dtype=np.float32),
(height, width, 3),
)
pixel_intrinsics = intrinsics[0].copy()
pixel_intrinsics[0] *= width
pixel_intrinsics[1] *= height
pixel_intrinsics[2] = (0.0, 0.0, 1.0)
frame = GeometryFrame(
depth=depth[0],
points=points[0],
intrinsics=pixel_intrinsics,
valid_mask=np.isfinite(depth[0]) & (depth[0] > 0.0),
model_mask=mask[0],
)
write_surface_archive(build_surface(frame), mesh_path)
normal_path = write_test_normal_texture(
normal,
directory / "normal.png",
)
surface = build_image_mesh(
bpy.context,
source,
mesh_path,
False,
True,
False,
True,
color_image=foreground,
normal_path=normal_path,
)
assert len(surface.data.vertices) > 0
assert all(len(polygon.vertices) == 3 for polygon in surface.data.polygons)
assert surface.modifiers[0].node_group.name == MOGE_NODE_GROUP_NAME
inputs = {
item.name
for item in surface.modifiers[0].node_group.interface.items_tree
if getattr(item, "in_out", None) == "INPUT"
}
assert "Stretch" not in inputs
assert "Depth Continuity" in inputs
assert "Mask" in inputs
assert "Confidence" not in inputs
assert "o_mask" in surface.data.attributes
assert surface.data.attributes["o_mask"].domain == "FACE"
assert not all(item.value for item in surface.data.attributes["o_mask"].data)
attribute_names = set(surface.data.attributes.keys())
assert attribute_names >= {"o_depth", "o_mask"}
assert "o_depth_discontinuity" in attribute_names
assert surface.data.attributes["o_depth_discontinuity"].data_type == "FLOAT"
assert "o_edge_stretch" not in attribute_names
assert not {
"o_confidence",
"o_confidence_percentile",
"o_confidence",
"o_background",
"o_background_face",
}.intersection(attribute_names)
mask_socket = next(
item
for item in surface.modifiers[0].node_group.interface.items_tree
if getattr(item, "in_out", None) == "INPUT" and item.name == "Mask"
)
assert mask_socket.default_value is False
assert mask_socket.structure_type == "SINGLE"
assert mask_socket.parent.name == "Culling"
depth_edge_socket = next(
item
for item in surface.modifiers[0].node_group.interface.items_tree
if getattr(item, "in_out", None) == "INPUT" and item.name == "Depth Continuity"
)
assert depth_edge_socket.default_value == 50.0
assert depth_edge_socket.subtype == "PERCENTAGE"
assert depth_edge_socket.structure_type == "SINGLE"
culling_panel = next(
item
for item in surface.modifiers[0].node_group.interface.items_tree
if item.item_type == "PANEL" and item.name == "Culling"
)
assert [item.name for item in culling_panel.interface_items] == [
"Depth Continuity",
"Mask",
]
input_sockets = [
item
for item in surface.modifiers[0].node_group.interface.items_tree
if item.item_type == "SOCKET" and item.in_out == "INPUT"
]
assert all(item.structure_type == "SINGLE" for item in input_sockets)
resolution_socket = next(
item for item in input_sockets if item.name == "Resolution"
)
assert abs(resolution_socket.default_value - 0.1) < 1e-6
smooth_socket = next(
item
for item in surface.modifiers[0].node_group.interface.items_tree
if getattr(item, "in_out", None) == "INPUT" and item.name == "Smooth"
)
assert smooth_socket.default_value == 0.5
assert not any(
node.bl_idname == "GeometryNodeInputNamedAttribute"
and node.inputs["Name"].default_value == "o_confidence"
for node in surface.modifiers[0].node_group.nodes
)
assert MOGE_SURFACE_MATERIAL_NODE_GROUP_NAME in bpy.data.node_groups
material_group = bpy.data.node_groups[MOGE_SURFACE_MATERIAL_NODE_GROUP_NAME]
assert any(node.bl_idname == "ShaderNodeBump" for node in material_group.nodes)
normal_maps = [
node for node in material_group.nodes if node.bl_idname == "ShaderNodeNormalMap"
]
assert {node.space for node in normal_maps} == {"TANGENT", "OBJECT"}
normal_strength = next(
item
for item in material_group.interface.items_tree
if getattr(item, "in_out", None) == "INPUT" and item.name == "Normal Scale"
)
assert normal_strength.default_value == 1.0
assert normal_strength.min_value == 0.0
normal_input = next(
item
for item in material_group.interface.items_tree
if getattr(item, "in_out", None) == "INPUT" and item.name == "Normal"
)
assert tuple(normal_input.default_value) == (0.5, 0.5, 1.0, 1.0)
assert all(node.inputs["Strength"].is_linked for node in normal_maps)
layer = next(
node
for node in surface.data.materials[0].node_tree.nodes
if node.bl_idname == "ShaderNodeGroup" and node.node_tree == material_group
)
assert layer.inputs["Object Space"].default_value is True
assert not surface.data.has_custom_normals
color_texture = next(
node
for node in surface.data.materials[0].node_tree.nodes
if node.bl_idname == "ShaderNodeTexImage" and node.image == foreground
)
assert color_texture.image.name == "MoGe Surface Test.color"
assert color_texture.image.colorspace_settings.name == material_color_space_name(
bpy.context.scene
)
normal_texture = next(
node
for node in surface.data.materials[0].node_tree.nodes
if node.bl_idname == "ShaderNodeTexImage"
and node.image.colorspace_settings.name == "Non-Color"
)
assert normal_texture.image.name == "MoGe Surface Test.normal"
assert any(child.type == "CAMERA" for child in surface.children)
print("MoGe-2 surface validation passed")
def validate_real_result(results_path, image_path):
image = bpy.data.images.load(str(image_path), check_existing=False)
source = bpy.data.objects.new("MoGe Real Surface", None)
source.empty_display_type = "IMAGE"
source.data = image
bpy.context.collection.objects.link(source)
mesh_path = results_path.with_name("mesh.npz")
if not mesh_path.is_file():
raise ValueError(f"Surface archive is missing: {mesh_path}")
normal_path = results_path.with_name("normal.png")
if not normal_path.is_file():
raise ValueError(f"Surface normal texture is missing: {normal_path}")
color_image = bpy.data.images.load(str(image_path), check_existing=False)
color_image.name = f"{image_path.stem}.color"
surface = build_image_mesh(
bpy.context,
source,
mesh_path,
False,
True,
False,
True,
color_image=color_image,
normal_path=normal_path,
)
assert len(surface.data.vertices) > 100
assert len(surface.data.polygons) > 100
assert surface.modifiers[0].node_group.name == MOGE_NODE_GROUP_NAME
assert MOGE_SURFACE_MATERIAL_NODE_GROUP_NAME in bpy.data.node_groups
assert not surface.data.has_custom_normals
material_group = bpy.data.node_groups[MOGE_SURFACE_MATERIAL_NODE_GROUP_NAME]
assert any(node.bl_idname == "ShaderNodeBump" for node in material_group.nodes)
normal_maps = [
node for node in material_group.nodes if node.bl_idname == "ShaderNodeNormalMap"
]
assert {node.space for node in normal_maps} == {"TANGENT", "OBJECT"}
assert all(node.inputs["Strength"].is_linked for node in normal_maps)
layer = next(
node
for node in surface.data.materials[0].node_tree.nodes
if node.bl_idname == "ShaderNodeGroup" and node.node_tree == material_group
)
assert layer.inputs["Object Space"].default_value is True
attribute_names = set(surface.data.attributes.keys())
assert {"o_depth", "o_mask"}.issubset(attribute_names)
assert "o_depth_discontinuity" in attribute_names
assert "o_edge_stretch" not in attribute_names
assert not {
"o_confidence",
"o_confidence_percentile",
"o_confidence",
"o_background",
"o_background_face",
}.intersection(attribute_names)
camera = next(child for child in surface.children if child.type == "CAMERA")
dimensions = surface.dimensions
assert max(dimensions) < 10.0
assert min(dimensions.x, dimensions.y) > 0.1
assert 1.0 <= camera.data.lens <= 500.0
assert abs(camera.data.shift_x) <= 1.0
assert abs(camera.data.shift_y) <= 1.0
print(
f"Real MoGe-2 surface validation passed: "
f"{len(surface.data.vertices)} vertices, {len(surface.data.polygons)} faces, "
f"dimensions={tuple(round(value, 4) for value in dimensions)}, "
f"lens={camera.data.lens:.3f}, shift=({camera.data.shift_x:.4f}, "
f"{camera.data.shift_y:.4f})"
)
if __name__ == "__main__":
main()