Add geometry nodes asset and validation script
- Implemented a new geometry nodes asset in `build_geometry_nodes_asset.py` to handle mesh refinement and topology fixing. - Created a validation script in `validate_geometry_nodes_asset.py` to ensure the integrity and functionality of the geometry node group. - Added input handling for various parameters including resolution, iterations, and smoothing options. - Included checks for node group structure and attributes to validate the geometry processing pipeline.
This commit is contained in:
@@ -8,6 +8,8 @@ from tempfile import TemporaryDirectory
|
||||
from types import ModuleType, SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
SOURCE_ROOT = PROJECT_ROOT / "src"
|
||||
@@ -28,6 +30,7 @@ class AddonTest(unittest.TestCase):
|
||||
)
|
||||
fake_bpy.types = SimpleNamespace(
|
||||
AddonPreferences=object,
|
||||
Menu=object,
|
||||
Operator=object,
|
||||
Panel=object,
|
||||
PropertyGroup=object,
|
||||
@@ -79,6 +82,7 @@ class AddonTest(unittest.TestCase):
|
||||
self.context_menu_draws.clear()
|
||||
self.addon.operators._active_job = None
|
||||
self.addon.image_depth._last_depth_plane_model = "BASE"
|
||||
self.addon.image_mesh._last_mesh_model = "BASE"
|
||||
self.fake_bpy.data = SimpleNamespace(
|
||||
objects=SimpleNamespace(get=lambda _name: None),
|
||||
)
|
||||
@@ -125,6 +129,135 @@ class AddonTest(unittest.TestCase):
|
||||
)
|
||||
self.assertFalse(hasattr(self.addon, "ImportDepthPlane"))
|
||||
|
||||
def test_registers_perspective_mesh_operators(self):
|
||||
self.assertIn(self.addon.ConvertImageToMesh, self.addon.CLASSES)
|
||||
self.assertIn(self.addon.BuildImageMesh, self.addon.CLASSES)
|
||||
self.assertEqual(self.addon.BuildImageMesh.bl_options, {"INTERNAL"})
|
||||
self.assertIn(self.addon.SnapFormImageMenu, self.addon.CLASSES)
|
||||
|
||||
def test_image_context_menu_groups_actions_and_separates_background(self):
|
||||
calls = []
|
||||
|
||||
class Layout:
|
||||
operator_context = None
|
||||
|
||||
def operator(self, identifier, **_options):
|
||||
calls.append(identifier)
|
||||
|
||||
def separator(self):
|
||||
calls.append("separator")
|
||||
|
||||
menu = self.addon.SnapFormImageMenu()
|
||||
menu.layout = Layout()
|
||||
menu.draw(None)
|
||||
|
||||
self.assertEqual(
|
||||
calls,
|
||||
[
|
||||
"snapform.convert_image_to_depth_plane",
|
||||
"snapform.convert_image_to_mesh",
|
||||
"separator",
|
||||
"snapform.remove_image_background",
|
||||
],
|
||||
)
|
||||
|
||||
def test_image_mesh_reference_plane_matches_image_empty_bounds(self):
|
||||
source_object = SimpleNamespace(
|
||||
data=SimpleNamespace(size=(401, 201)),
|
||||
empty_display_size=4.0,
|
||||
empty_image_offset=(-0.25, -0.75),
|
||||
)
|
||||
intrinsics = np.asarray(
|
||||
[[200.0, 0.0, 200.0], [0.0, 200.0, 100.0], [0.0, 0.0, 1.0]],
|
||||
dtype=np.float32,
|
||||
)
|
||||
camera_vertices = np.asarray(
|
||||
[
|
||||
[-2.0, -1.0, 2.0],
|
||||
[2.0, -1.0, 2.0],
|
||||
[-2.0, 1.0, 2.0],
|
||||
[2.0, 1.0, 2.0],
|
||||
],
|
||||
dtype=np.float32,
|
||||
)
|
||||
|
||||
vertices, camera = self.addon.image_mesh.empty_anchored_vertices(
|
||||
source_object,
|
||||
camera_vertices,
|
||||
intrinsics,
|
||||
(401, 201),
|
||||
2.0,
|
||||
)
|
||||
|
||||
x_min, x_max, y_min, y_max = self.addon.image_depth.plane_bounds(
|
||||
source_object
|
||||
)
|
||||
np.testing.assert_allclose(
|
||||
vertices,
|
||||
[
|
||||
[x_min, y_max, 0.0],
|
||||
[x_max, y_max, 0.0],
|
||||
[x_min, y_min, 0.0],
|
||||
[x_max, y_min, 0.0],
|
||||
],
|
||||
)
|
||||
np.testing.assert_allclose(
|
||||
camera["location"],
|
||||
((x_min + x_max) * 0.5, (y_min + y_max) * 0.5, 2.0024922),
|
||||
)
|
||||
|
||||
def test_mesh_conversion_dialog_only_keeps_model_and_camera_options(self):
|
||||
annotations = self.addon.ConvertImageToMesh.__annotations__
|
||||
|
||||
for property_name in (
|
||||
"snapform_confidence_percentile",
|
||||
"snapform_edge_culling",
|
||||
"snapform_face_reduction",
|
||||
"snapform_depth_smoothing",
|
||||
"snapform_remove_background",
|
||||
"snapform_postprocess_method",
|
||||
):
|
||||
with self.subTest(property_name=property_name):
|
||||
self.assertNotIn(property_name, annotations)
|
||||
self.assertFalse(annotations["snapform_create_camera"]["default"])
|
||||
self.assertFalse(annotations["snapform_unshaded"]["default"])
|
||||
self.assertEqual(annotations["snapform_unshaded"]["name"], "Unshaded")
|
||||
|
||||
def test_image_mesh_uses_o_prefixed_attributes_and_bundled_nodes(self):
|
||||
image_mesh = self.addon.image_mesh
|
||||
names = set(image_mesh.POINT_ATTRIBUTES) | set(
|
||||
image_mesh.FACE_ATTRIBUTES
|
||||
)
|
||||
|
||||
self.assertTrue(all(name.startswith("o_") for name in names))
|
||||
self.assertTrue(image_mesh.node_asset_path().is_file())
|
||||
self.assertEqual(image_mesh.NODE_GROUP_NAME, "O Refine DA3 Mesh")
|
||||
self.assertEqual(image_mesh.NODE_ASSET_NAME, "O_SnapForm.blend")
|
||||
|
||||
def test_image_mesh_material_depth_scale_matches_fitted_geometry(self):
|
||||
mesh_object = SimpleNamespace(scale=SimpleNamespace(z=-2.0))
|
||||
|
||||
self.assertEqual(
|
||||
self.addon.image_mesh.material_depth_scale(
|
||||
mesh_object,
|
||||
{"scale_z": 0.25},
|
||||
),
|
||||
0.5,
|
||||
)
|
||||
|
||||
def test_image_mesh_default_resolution_uses_double_reduction_estimate(self):
|
||||
vertices = np.asarray(
|
||||
((0.0, 0.0, 0.0), (1.0, 0.0, 0.0), (0.0, 1.0, 0.0)),
|
||||
dtype=np.float32,
|
||||
)
|
||||
faces = np.asarray(((0, 1, 2),), dtype=np.int32)
|
||||
edge_lengths = np.asarray((1.0, 1.0, np.sqrt(2.0)))
|
||||
|
||||
self.assertAlmostEqual(
|
||||
self.addon.image_mesh.default_remesh_resolution(vertices, faces),
|
||||
float(np.median(edge_lengths) * np.sqrt(10.0) * 2.0),
|
||||
)
|
||||
|
||||
def test_registers_image_background_operators(self):
|
||||
self.assertIn(self.addon.RemoveImageBackground, self.addon.CLASSES)
|
||||
self.assertIn(self.addon.ApplyRemovedBackground, self.addon.CLASSES)
|
||||
@@ -1486,6 +1619,51 @@ class AddonTest(unittest.TestCase):
|
||||
)
|
||||
self.assertEqual(parameters["use_ray_pose"], 0)
|
||||
|
||||
def test_reconstruct_request_enables_ben2_for_image_mesh(self):
|
||||
runtime = importlib.import_module("snapform.runtime")
|
||||
settings = SimpleNamespace(
|
||||
input_path="input.png",
|
||||
model="BASE",
|
||||
gs_model="GIANT_1_1",
|
||||
process_res=504,
|
||||
process_res_method="upper_bound_resize",
|
||||
conf_thresh_percentile=40.0,
|
||||
num_max_points=1_000_000,
|
||||
show_cameras=True,
|
||||
gs_views_interval=1,
|
||||
video_frames=16,
|
||||
device="AUTO",
|
||||
use_ray_pose=False,
|
||||
ref_view_strategy="saddle_balanced",
|
||||
)
|
||||
with patch.object(
|
||||
runtime,
|
||||
"runtime_python",
|
||||
return_value=Path("python"),
|
||||
), patch.object(
|
||||
runtime,
|
||||
"model_directory",
|
||||
side_effect=lambda key: Path(key.lower()),
|
||||
):
|
||||
request = runtime.reconstruct_request(
|
||||
settings,
|
||||
"mesh_data",
|
||||
Path("status.json"),
|
||||
Path("result"),
|
||||
remove_background=True,
|
||||
)
|
||||
|
||||
parameters = request.payload["parameters"]
|
||||
self.assertNotIn("mesh_confidence_percentile", parameters)
|
||||
self.assertNotIn("mesh_edge_culling", parameters)
|
||||
self.assertNotIn("mesh_face_reduction", parameters)
|
||||
self.assertNotIn("mesh_depth_smoothing", parameters)
|
||||
self.assertNotIn("mesh_postprocess_method", parameters)
|
||||
self.assertEqual(
|
||||
parameters["remove_background_model_dir"],
|
||||
str(Path("ben2_base")),
|
||||
)
|
||||
|
||||
def test_export_format_property_includes_all_da3_formats(self):
|
||||
export_format = self.addon.SnapFormSettings.__annotations__[
|
||||
"export_format"
|
||||
|
||||
Reference in New Issue
Block a user