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:
2026-08-11 13:23:13 +08:00
parent fc27c23fd1
commit 07d836d9ad
15 changed files with 2408 additions and 68 deletions

View File

@@ -982,7 +982,14 @@ class WorkerTest(unittest.TestCase):
"export_textured_mesh_glb",
side_effect=fake_mesh_export,
):
for export_format in self.worker.EXPORT_FORMATS:
debug_formats = (
"glb",
"gs_ply",
"depth_vis",
"mini_npz",
"mesh",
)
for export_format in debug_formats:
args = SimpleNamespace(
input=str(input_path),
video_frames=16,
@@ -1172,6 +1179,124 @@ class WorkerTest(unittest.TestCase):
self.assertEqual(threshold, 2.5)
def test_zero_mesh_confidence_disables_confidence_filter(self):
depth = np.ones((2, 2), dtype=np.float32)
confidence = np.full((2, 2), np.nan, dtype=np.float32)
x, y = np.meshgrid([0.0, 1.0], [0.0, 1.0])
points = np.stack((x, y, depth), axis=-1)
threshold = self.worker.confidence_percentile_threshold(
depth,
confidence,
0.0,
)
faces = self.worker.mesh_triangle_faces(
points,
depth,
confidence,
threshold,
filter_edges=False,
max_edge_length=0.0,
)
self.assertIsNone(threshold)
self.assertEqual(len(faces), 2)
def test_mesh_filters_triangles_independently(self):
depth = np.ones((2, 2), dtype=np.float32)
confidence = np.ones((2, 2), dtype=np.float32)
confidence[1, 0] = 0.0
x, y = np.meshgrid(
np.arange(2, dtype=np.float32),
np.arange(2, dtype=np.float32),
)
points = np.stack((x, y, depth), axis=-1)
faces = self.worker.mesh_triangle_faces(
points,
depth,
confidence,
confidence_threshold=0.5,
filter_edges=False,
max_edge_length=0.0,
)
np.testing.assert_array_equal(faces, [[0, 3, 1]])
def test_mesh_background_mask_combines_with_confidence(self):
depth = np.ones((2, 3), dtype=np.float32)
confidence = np.ones((2, 3), dtype=np.float32)
x, y = np.meshgrid(
np.arange(3, dtype=np.float32),
np.arange(2, dtype=np.float32),
)
points = np.stack((x, y, depth), axis=-1)
foreground = np.ones((2, 3), dtype=bool)
foreground[:, 0] = False
faces = self.worker.mesh_triangle_faces(
points,
depth,
confidence,
confidence_threshold=None,
filter_edges=False,
max_edge_length=0.0,
vertex_mask=foreground,
)
np.testing.assert_array_equal(faces, [[1, 4, 5], [1, 5, 2]])
def test_depth_edge_filter_ignores_smooth_slope(self):
smooth = np.tile(
np.linspace(2.0, 2.04, 5, dtype=np.float32),
(3, 1),
)
discontinuous = smooth.copy()
discontinuous[:, 3:] += 1.0
self.assertFalse(self.worker.depth_edge_mask(smooth).any())
self.assertTrue(self.worker.depth_edge_mask(discontinuous)[:, 2:4].all())
def test_higher_edge_culling_allows_less_stretch(self):
limits = [
self.worker.edge_culling_stretch_limit(value)
for value in (0.0, 50.0, 100.0)
]
self.assertEqual(limits, [10000.0, 2.0, 1.0])
def test_stretched_edge_culling_uses_camera_space_edge_ratio(self):
flat_points = np.asarray(
[
[[0.0, 0.0, 1.0], [1.0, 0.0, 1.0]],
[[0.0, 1.0, 1.0], [1.0, 1.0, 1.0]],
],
dtype=np.float32,
)
stretched_points = flat_points.copy()
stretched_points[0, 1] = (2.0, 0.0, 2.0)
triangles = np.asarray([[0, 2, 3], [0, 3, 1]], dtype=np.int32)
flat_mask = self.worker.triangle_edge_stretch_mask(
flat_points,
triangles,
1.0,
)
loose_mask = self.worker.triangle_edge_stretch_mask(
stretched_points,
triangles,
2.0,
)
strict_mask = self.worker.triangle_edge_stretch_mask(
stretched_points,
triangles,
1.0,
)
self.assertTrue(flat_mask.all())
self.assertTrue(loose_mask.all())
self.assertFalse(strict_mask.all())
def test_mesh_faces_apply_confidence_threshold_and_face_direction(self):
points = np.array(
[
@@ -1228,6 +1353,113 @@ class WorkerTest(unittest.TestCase):
self.assertEqual(face_counts, [8, 2, 0])
def test_mesh_compaction_removes_unreferenced_vertices(self):
vertices = np.arange(18, dtype=np.float32).reshape(6, 3)
faces = np.asarray([[1, 3, 5]], dtype=np.int32)
compact_vertices, compact_faces = self.worker.compact_mesh(
vertices,
faces,
)
np.testing.assert_array_equal(
compact_vertices,
vertices[[1, 3, 5]],
)
np.testing.assert_array_equal(compact_faces, [[0, 1, 2]])
def test_image_mesh_data_keeps_camera_geometry_and_uv(self):
class FakeImage:
def __enter__(self):
return self
def __exit__(self, *_args):
return None
def convert(self, mode):
self.mode = mode
return self
def save(self, path, **_options):
Path(path).write_bytes(b"texture")
fake_pil = ModuleType("PIL")
fake_pil.Image = SimpleNamespace(open=lambda _path: FakeImage())
depth = np.full((1, 3, 3), 2.0, dtype=np.float32)
confidence = np.ones((1, 3, 3), dtype=np.float32)
intrinsics = np.asarray(
[[[2.0, 0.0, 1.0], [0.0, 2.0, 1.0], [0.0, 0.0, 1.0]]],
dtype=np.float32,
)
prediction = SimpleNamespace(
depth=depth,
conf=confidence,
intrinsics=intrinsics,
)
progress = []
with tempfile.TemporaryDirectory() as directory, patch.dict(
sys.modules,
{"PIL": fake_pil},
), patch.object(
self.worker,
"compact_mesh",
side_effect=lambda vertices, faces: (
vertices,
faces,
),
):
result_path, texture_path = self.worker.export_image_mesh_data(
prediction,
Path(directory) / "input.png",
Path(directory) / "result",
progress_callback=lambda step, total, label: progress.append(
(step, total, label)
),
)
with np.load(result_path) as result:
self.assertEqual(result["vertices"].shape, (9, 3))
self.assertEqual(result["faces"].shape, (8, 3))
self.assertEqual(result["uv"].shape, (9, 2))
self.assertEqual(float(result["reference_depth"]), 2.0)
self.assertNotIn("postprocess_method", result.files)
self.assertEqual(result["point_depth"].shape, (9,))
self.assertEqual(
result["point_confidence_percentile"].shape,
(9,),
)
self.assertEqual(
result["face_edge_stretch"].shape,
(8,),
)
self.assertEqual(result["face_background"].shape, (8,))
np.testing.assert_allclose(result["uv"][0], [0.0, 1.0])
np.testing.assert_allclose(result["uv"][-1], [1.0, 0.0])
self.assertTrue(texture_path.is_file())
self.assertEqual([item[0] for item in progress], list(range(1, 7)))
self.assertTrue(all(item[1] == 6 for item in progress))
def test_prediction_attributes_keep_debug_structure(self):
prediction = SimpleNamespace(
depth=np.ones((1, 2, 3), dtype=np.float32),
conf=np.arange(6, dtype=np.float32).reshape(1, 2, 3),
intrinsics=np.eye(3, dtype=np.float32)[None],
extrinsics=np.eye(4, dtype=np.float32)[None],
)
with tempfile.TemporaryDirectory() as directory:
output_path = self.worker.export_prediction_attributes(
prediction,
directory,
)
with np.load(output_path) as attributes:
self.assertEqual(attributes["depth"].shape, (1, 2, 3))
self.assertEqual(attributes["conf"].shape, (1, 2, 3))
self.assertEqual(
attributes["confidence_percentile"].shape,
(1, 2, 3),
)
if __name__ == "__main__":
unittest.main()