feat: Add MoGe-2 surface generation and export functionality
- Introduced new enum items for depth plane and surface models with caching. - Added MOGE2_MESH_JOB to operators for converting images to surfaces. - Implemented surface mesh building and exporting in RunJob operator. - Enhanced UI to include export format options for MoGe-2. - Created validation tools for MoGe-2 surface assets and depth planes. - Updated geometry nodes to support new surface generation features. - Refactored existing tests to accommodate new functionality and ensure stability.
This commit is contained in:
176
tools/validate_moge_surface.py
Normal file
176
tools/validate_moge_surface.py
Normal file
@@ -0,0 +1,176 @@
|
||||
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"))
|
||||
|
||||
from snapform.image_depth import MOGE_SURFACE_MATERIAL_NODE_GROUP_NAME
|
||||
from snapform.image_mesh import (
|
||||
MOGE_NODE_GROUP_NAME,
|
||||
build_image_mesh,
|
||||
prepare_moge_surface_archive,
|
||||
)
|
||||
|
||||
|
||||
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)
|
||||
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)
|
||||
results_path = directory / "results.npz"
|
||||
mesh_path = directory / "mesh.npz"
|
||||
np.savez(
|
||||
results_path,
|
||||
depth=depth,
|
||||
mask=mask,
|
||||
points=points,
|
||||
intrinsics=intrinsics,
|
||||
)
|
||||
prepare_moge_surface_archive(results_path, mesh_path)
|
||||
surface = build_image_mesh(
|
||||
bpy.context,
|
||||
source,
|
||||
mesh_path,
|
||||
False,
|
||||
True,
|
||||
False,
|
||||
False,
|
||||
True,
|
||||
)
|
||||
assert len(surface.data.vertices) > 0
|
||||
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 "Mask" in inputs
|
||||
assert "Confidence" not in inputs
|
||||
assert "o_mask" in surface.data.attributes
|
||||
assert surface.data.attributes["o_mask"].domain == "FACE"
|
||||
attribute_names = set(surface.data.attributes.keys())
|
||||
assert attribute_names >= {"o_depth", "o_mask"}
|
||||
assert "o_edge_stretch" not in attribute_names
|
||||
assert not {
|
||||
"o_confidence",
|
||||
"o_confidence_percentile",
|
||||
"o_face_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 True
|
||||
assert mask_socket.structure_type == "SINGLE"
|
||||
assert not mask_socket.parent.name
|
||||
assert not any(
|
||||
item.item_type == "PANEL" and item.name == "Culling"
|
||||
for item in surface.modifiers[0].node_group.interface.items_tree
|
||||
)
|
||||
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_face_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)
|
||||
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():
|
||||
prepare_moge_surface_archive(results_path, mesh_path)
|
||||
surface = build_image_mesh(
|
||||
bpy.context,
|
||||
source,
|
||||
mesh_path,
|
||||
False,
|
||||
True,
|
||||
False,
|
||||
False,
|
||||
True,
|
||||
)
|
||||
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 surface.data.has_custom_normals
|
||||
normals = np.empty(len(surface.data.corner_normals) * 3, dtype=np.float32)
|
||||
surface.data.corner_normals.foreach_get("vector", normals)
|
||||
normals = normals.reshape(-1, 3)
|
||||
assert np.isfinite(normals).all()
|
||||
assert np.min(np.linalg.norm(normals, axis=1)) > 0.99
|
||||
material_group = bpy.data.node_groups[MOGE_SURFACE_MATERIAL_NODE_GROUP_NAME]
|
||||
assert any(node.bl_idname == "ShaderNodeBump" for node in material_group.nodes)
|
||||
attribute_names = set(surface.data.attributes.keys())
|
||||
assert {"o_depth", "o_mask"}.issubset(attribute_names)
|
||||
assert "o_edge_stretch" not in attribute_names
|
||||
assert not {
|
||||
"o_confidence",
|
||||
"o_confidence_percentile",
|
||||
"o_face_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()
|
||||
Reference in New Issue
Block a user