Add new node asset scripts for geometry and shader operations
- Implemented `build_mesh_plane.py` to create a geometry node group for mesh planes with customizable parameters. - Created `build_shadeless.py` for a shader node group that displays color without scene lighting while preserving shadows and reflections. - Added `validate.py` to validate the integrity of the newly created node groups and their inputs. - Introduced `validate_cutout.py` to ensure the functionality of cutout node groups and their dependencies. - Developed `validate_mesh_plane.py` to test the mesh plane creation and its modifier settings.
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
import importlib.util
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
@@ -116,76 +115,29 @@ class WorkerTest(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.media = load_server_module("media.input")
|
||||
cls.resolution = load_server_module("media.resolution")
|
||||
cls.geometry = load_server_module("geometry")
|
||||
cls.surface = load_server_module("geometry.surface")
|
||||
cls.background = load_server_module("jobs.background")
|
||||
cls.background = load_server_module("jobs.remove_background")
|
||||
cls.ben2 = load_server_module("models.ben2")
|
||||
cls.da3_model = load_server_module("models.da3")
|
||||
cls.moge2_model = load_server_module("models.moge2")
|
||||
cls.upscale_model = load_server_module("models.upscale")
|
||||
cls.onnx_upscale = load_server_module("models.onnx_upscale")
|
||||
cls.surface_outputs = load_server_module("outputs.surface")
|
||||
cls.debug_da3_outputs = load_server_module("outputs.debug_da3")
|
||||
cls.debug_moge2_outputs = load_server_module("outputs.debug_moge2")
|
||||
cls.common_job = load_server_module("jobs.common")
|
||||
cls.depth_job = load_server_module("jobs.depth")
|
||||
cls.surface_job = load_server_module("jobs.surface")
|
||||
cls.debug_da3_job = load_server_module("jobs.debug_da3")
|
||||
cls.debug_moge2_job = load_server_module("jobs.debug_moge2")
|
||||
cls.debug_ben2_job = load_server_module("jobs.debug_ben2")
|
||||
cls.debug_upscale_job = load_server_module(
|
||||
"jobs.debug_upscale"
|
||||
)
|
||||
cls.debug_jobs = load_server_module("jobs.debug_predictions")
|
||||
cls.upscale_job = load_server_module("jobs.upscale")
|
||||
cls.frames_job = load_server_module("jobs.frames")
|
||||
cls.foreground_job = load_server_module("jobs.foreground")
|
||||
cls.download = load_server_module("model_download")
|
||||
|
||||
def test_inference_progress_omits_single_frame_count(self):
|
||||
def test_upscale_progress_omits_single_frame_count(self):
|
||||
self.assertEqual(
|
||||
self.common_job.frame_progress_message(
|
||||
"Estimating MoGe-2 geometry", 0, 1
|
||||
),
|
||||
"Estimating MoGe-2 geometry",
|
||||
self.upscale_job.frame_progress_message("Upscaling image", 0, 1),
|
||||
"Upscaling image",
|
||||
)
|
||||
self.assertEqual(
|
||||
self.common_job.frame_progress_message(
|
||||
"Estimating DA3 geometry", 0, 1
|
||||
),
|
||||
"Estimating DA3 geometry",
|
||||
self.upscale_job.frame_progress_message("Upscaling image", 1, 3),
|
||||
"Upscaling image (2/3)",
|
||||
)
|
||||
self.assertEqual(
|
||||
self.common_job.frame_progress_message(
|
||||
"Estimating DA3 geometry", 1, 3
|
||||
),
|
||||
"Estimating DA3 geometry (2/3)",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
self.common_job.frame_progress_message(
|
||||
"Estimating MoGe-2 depth", 0, 1
|
||||
),
|
||||
"Estimating MoGe-2 depth",
|
||||
)
|
||||
self.assertEqual(
|
||||
self.common_job.frame_progress_message(
|
||||
"Estimating DA3 depth", 0, 1
|
||||
),
|
||||
"Estimating DA3 depth",
|
||||
)
|
||||
|
||||
def test_surface_has_one_model_independent_builder(self):
|
||||
self.assertTrue(callable(self.surface.build_surface))
|
||||
self.assertTrue(callable(self.surface_outputs.write_surface_archive))
|
||||
self.assertFalse(hasattr(self.surface_outputs, "write_surface_texture"))
|
||||
self.assertFalse(hasattr(self.surface, "export_da3_surface"))
|
||||
self.assertFalse(hasattr(self.surface, "export_moge2_surface"))
|
||||
|
||||
def test_model_adapters_only_normalize_surface_input(self):
|
||||
self.assertTrue(callable(self.da3_model.surface_frame))
|
||||
self.assertTrue(callable(self.moge2_model.surface_frame))
|
||||
self.assertFalse(hasattr(self.da3_model, "generate_surface"))
|
||||
self.assertFalse(hasattr(self.moge2_model, "generate_surface"))
|
||||
|
||||
def test_collect_images_is_sorted_and_filters_suffixes(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
@@ -740,35 +692,6 @@ class WorkerTest(unittest.TestCase):
|
||||
for _image, options in inference_options:
|
||||
self.assertEqual(options, {})
|
||||
|
||||
def test_extract_frames_copies_selected_frames(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
input_directory = root / "frames"
|
||||
input_directory.mkdir()
|
||||
for index in range(1, 6):
|
||||
(input_directory / f"frame-{index:04d}.jpg").touch()
|
||||
context = FakeJobContext(root / "result", "extract-frames")
|
||||
self.frames_job.run(
|
||||
context,
|
||||
{
|
||||
"input": str(input_directory),
|
||||
"video_frames": 3,
|
||||
"start_frame": 2,
|
||||
},
|
||||
)
|
||||
|
||||
result_directory = context.directory
|
||||
self.assertEqual(
|
||||
sorted(
|
||||
path.name
|
||||
for path in result_directory.iterdir()
|
||||
if path.is_file()
|
||||
),
|
||||
["image.0001.jpg", "image.0002.jpg", "image.0003.jpg"],
|
||||
)
|
||||
self.assertEqual(context.status["progress"], 1.0)
|
||||
self.assertIn("3 frame", context.status["message"])
|
||||
|
||||
def test_da3_debug_saves_only_direct_api_prediction(self):
|
||||
prediction = SimpleNamespace(
|
||||
depth=np.full((1, 2, 2), 2.0, dtype=np.float32),
|
||||
@@ -802,7 +725,7 @@ class WorkerTest(unittest.TestCase):
|
||||
},
|
||||
):
|
||||
context = FakeJobContext(root / "result", "debug-da3")
|
||||
self.debug_da3_job.run(
|
||||
self.debug_jobs.run_da3(
|
||||
context,
|
||||
{
|
||||
"input": str(input_path),
|
||||
@@ -848,11 +771,11 @@ class WorkerTest(unittest.TestCase):
|
||||
(frames / "b.png").touch()
|
||||
context = FakeJobContext(root / "result", "debug-moge2")
|
||||
with patch.object(
|
||||
self.debug_moge2_job,
|
||||
"infer_one",
|
||||
self.debug_jobs,
|
||||
"infer_moge2",
|
||||
return_value=prediction,
|
||||
) as infer_one:
|
||||
self.debug_moge2_job.run(
|
||||
self.debug_jobs.run_moge2(
|
||||
context,
|
||||
{
|
||||
"input": str(frames),
|
||||
@@ -880,11 +803,11 @@ class WorkerTest(unittest.TestCase):
|
||||
input_path.touch()
|
||||
context = FakeJobContext(root / "result", "debug-ben2")
|
||||
with patch.object(
|
||||
self.debug_ben2_job,
|
||||
self.debug_jobs,
|
||||
"infer_foreground",
|
||||
return_value=(Foreground(), 1.0, 2.0),
|
||||
) as infer_foreground:
|
||||
self.debug_ben2_job.run(
|
||||
self.debug_jobs.run_ben2(
|
||||
context,
|
||||
{
|
||||
"input": str(input_path),
|
||||
@@ -910,11 +833,11 @@ class WorkerTest(unittest.TestCase):
|
||||
(frames / "b.png").touch()
|
||||
context = FakeJobContext(root / "result", "debug-upscale")
|
||||
with patch.object(
|
||||
self.debug_upscale_job,
|
||||
"infer_one",
|
||||
self.debug_jobs,
|
||||
"infer_upscale",
|
||||
return_value=Image.new("RGB", (8, 8), (10, 20, 30)),
|
||||
) as infer_one:
|
||||
self.debug_upscale_job.run(
|
||||
self.debug_jobs.run_upscale(
|
||||
context,
|
||||
{
|
||||
"input": str(frames),
|
||||
@@ -967,7 +890,7 @@ class WorkerTest(unittest.TestCase):
|
||||
(
|
||||
"INFO",
|
||||
"Upscale model: REALESRGAN_GENERAL_WDN_X4V3; "
|
||||
"maximum output size: 4096 px",
|
||||
"maximum AI input size: 2048 px",
|
||||
),
|
||||
context.logs,
|
||||
)
|
||||
@@ -1009,24 +932,19 @@ class WorkerTest(unittest.TestCase):
|
||||
int(alpha[:, -1].min()),
|
||||
)
|
||||
|
||||
def test_upscale_input_is_limited_before_four_x_inference(self):
|
||||
def test_upscale_input_uses_the_global_long_edge_limit(self):
|
||||
from PIL import Image
|
||||
|
||||
source = Image.new("RGB", (2000, 1000))
|
||||
limited = self.upscale_model._limited_input(source, 4096)
|
||||
source = Image.new("RGB", (3000, 1500))
|
||||
limited = self.resolution.limit_image(source, 2048)
|
||||
|
||||
self.assertEqual(limited.size, (1024, 512))
|
||||
square = self.upscale_model._limited_input(
|
||||
Image.new("RGB", (3000, 3000)),
|
||||
8192,
|
||||
)
|
||||
self.assertLessEqual(
|
||||
square.width * square.height * 16,
|
||||
self.upscale_model.MAX_OUTPUT_PIXELS,
|
||||
self.assertEqual(limited.size, (2048, 1024))
|
||||
self.assertEqual(
|
||||
self.resolution.limit_image(Image.new("RGB", (800, 600)), 2048).size,
|
||||
(800, 600),
|
||||
)
|
||||
with self.assertRaisesRegex(ValueError, "between 1 and 8192"):
|
||||
self.upscale_model._limited_input(source, 9000)
|
||||
|
||||
self.resolution.limit_image(source, 9000)
|
||||
def test_upscale_resource_error_releases_session_and_is_readable(self):
|
||||
from PIL import Image
|
||||
|
||||
@@ -1057,495 +975,20 @@ class WorkerTest(unittest.TestCase):
|
||||
):
|
||||
with self.assertRaisesRegex(
|
||||
RuntimeError,
|
||||
"Reduce Max Output Size or select CPU",
|
||||
"Reduce Maximum AI Input Size or select CPU",
|
||||
):
|
||||
self.upscale_model.infer_one(
|
||||
models,
|
||||
{
|
||||
"model": "REALESRGAN_GENERAL_WDN_X4V3",
|
||||
"device": "directml",
|
||||
"max_output_size": 4096,
|
||||
"max_input_size": 2048,
|
||||
},
|
||||
path,
|
||||
)
|
||||
|
||||
self.assertTrue(models.released)
|
||||
|
||||
def test_da3_removes_background_after_depth_using_original_input(self):
|
||||
order = []
|
||||
inference_inputs = []
|
||||
fake_da3 = ModuleType("server.models.onnx_da3")
|
||||
|
||||
def fake_infer(_session, images, **_options):
|
||||
order.append("depth")
|
||||
inference_inputs.append(images)
|
||||
return SimpleNamespace(
|
||||
depth=np.full((1, 2, 2), 2.0, dtype=np.float32),
|
||||
conf=np.ones((1, 2, 2), dtype=np.float32),
|
||||
extrinsics=np.eye(4, dtype=np.float32)[None, :3],
|
||||
intrinsics=np.eye(3, dtype=np.float32)[None],
|
||||
processed_images=np.zeros((1, 2, 2, 3), dtype=np.uint8),
|
||||
)
|
||||
|
||||
fake_da3.infer = fake_infer
|
||||
fake_runtime = ModuleType("server.models.onnx_runtime")
|
||||
fake_runtime.provider_label = lambda _session: "CPU"
|
||||
|
||||
class FakeForeground:
|
||||
def convert(self, _mode):
|
||||
return self
|
||||
|
||||
def save(self, path, **_options):
|
||||
Path(path).touch()
|
||||
|
||||
def fake_foreground(image, *_args, **_kwargs):
|
||||
order.append("background")
|
||||
self.assertEqual(image, input_path)
|
||||
return FakeForeground(), 1.0, 2.0
|
||||
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
input_path = root / "input.png"
|
||||
input_path.touch()
|
||||
with patch.dict(
|
||||
sys.modules,
|
||||
{
|
||||
"server.models.onnx_da3": fake_da3,
|
||||
"server.models.onnx_runtime": fake_runtime,
|
||||
},
|
||||
), patch.object(
|
||||
self.foreground_job,
|
||||
"infer_foreground",
|
||||
side_effect=fake_foreground,
|
||||
):
|
||||
context = FakeJobContext(
|
||||
root / "result",
|
||||
"generate-depth",
|
||||
)
|
||||
self.depth_job.run(
|
||||
context,
|
||||
{
|
||||
"input": str(input_path),
|
||||
"model": "DA3",
|
||||
"remove_background_model": "BEN2",
|
||||
"video_frames": 1,
|
||||
"export_format": "mini_npz",
|
||||
"device": "cpu",
|
||||
"process_res": 518,
|
||||
"process_res_method": "upper_bound_resize",
|
||||
"ref_view_strategy": "first",
|
||||
"inference_mode": "per_frame",
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(order, ["depth", "background"])
|
||||
self.assertEqual(inference_inputs, [[str(input_path)]])
|
||||
|
||||
def test_reconstruct_copies_frames_for_multi_frame_input(self):
|
||||
inference_inputs = []
|
||||
fake_da3 = ModuleType("server.models.onnx_da3")
|
||||
fake_da3.create_session = lambda _model_dir, _device: object()
|
||||
|
||||
def fake_infer(_session, images, **_options):
|
||||
inference_inputs.append(images)
|
||||
return SimpleNamespace(
|
||||
depth=np.full((1, 2, 2), 2.0, dtype=np.float32),
|
||||
conf=np.ones((1, 2, 2), dtype=np.float32),
|
||||
extrinsics=np.eye(4, dtype=np.float32)[None, :3],
|
||||
intrinsics=np.eye(3, dtype=np.float32)[None],
|
||||
processed_images=np.zeros((1, 2, 2, 3), dtype=np.uint8),
|
||||
)
|
||||
|
||||
fake_da3.infer = fake_infer
|
||||
fake_runtime = ModuleType("server.models.onnx_runtime")
|
||||
fake_runtime.provider_label = lambda _session: "CPU"
|
||||
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
input_directory = root / "frames"
|
||||
input_directory.mkdir()
|
||||
(input_directory / "frame-0001.jpg").touch()
|
||||
(input_directory / "frame-0002.jpg").touch()
|
||||
with patch.dict(
|
||||
sys.modules,
|
||||
{
|
||||
"server.models.onnx_da3": fake_da3,
|
||||
"server.models.onnx_runtime": fake_runtime,
|
||||
},
|
||||
):
|
||||
context = FakeJobContext(
|
||||
root / "result",
|
||||
"generate-depth",
|
||||
)
|
||||
self.depth_job.run(
|
||||
context,
|
||||
{
|
||||
"input": str(input_directory),
|
||||
"model": "DA3",
|
||||
"video_frames": 16,
|
||||
"export_format": "mini_npz",
|
||||
"device": "cpu",
|
||||
"process_res": 518,
|
||||
"process_res_method": "upper_bound_resize",
|
||||
"ref_view_strategy": "first",
|
||||
"conf_thresh_percentile": 40.0,
|
||||
"num_max_points": 1000,
|
||||
"inference_mode": "per_frame",
|
||||
},
|
||||
)
|
||||
|
||||
result_directory = context.directory
|
||||
self.assertTrue(
|
||||
(result_directory / "image.0001.jpg").is_file()
|
||||
)
|
||||
self.assertTrue(
|
||||
(result_directory / "image.0002.jpg").is_file()
|
||||
)
|
||||
self.assertEqual(len(inference_inputs), 2)
|
||||
self.assertTrue(all(len(paths) == 1 for paths in inference_inputs))
|
||||
with np.load(result_directory / "results.npz") as result:
|
||||
self.assertEqual(result["depth"].shape, (2, 2, 2))
|
||||
self.assertTrue(
|
||||
(result_directory / "results.npz").is_file()
|
||||
)
|
||||
self.assertFalse(
|
||||
(result_directory / "exports").exists()
|
||||
)
|
||||
self.assertEqual(context.status["progress"], 1.0)
|
||||
|
||||
def test_mesh_confidence_uses_percentile(self):
|
||||
depth = np.ones((1, 2, 2), dtype=np.float32)
|
||||
confidence = np.array([[[1.0, 2.0], [3.0, 4.0]]])
|
||||
|
||||
threshold = self.geometry.confidence_percentile_threshold(
|
||||
depth,
|
||||
confidence,
|
||||
50.0,
|
||||
)
|
||||
|
||||
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.geometry.confidence_percentile_threshold(
|
||||
depth,
|
||||
confidence,
|
||||
0.0,
|
||||
)
|
||||
faces = self.geometry.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.geometry.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.geometry.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.geometry.depth_edge_mask(smooth).any())
|
||||
self.assertTrue(self.geometry.depth_edge_mask(discontinuous)[:, 2:4].all())
|
||||
|
||||
def test_higher_edge_culling_allows_less_stretch(self):
|
||||
limits = [
|
||||
self.geometry.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.geometry.triangle_edge_stretch_mask(
|
||||
flat_points,
|
||||
triangles,
|
||||
1.0,
|
||||
)
|
||||
loose_mask = self.geometry.triangle_edge_stretch_mask(
|
||||
stretched_points,
|
||||
triangles,
|
||||
2.0,
|
||||
)
|
||||
strict_mask = self.geometry.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(
|
||||
[
|
||||
[[0.0, 0.0, 1.0], [1.0, 0.0, 1.0], [2.0, 0.0, 1.0]],
|
||||
[[0.0, 1.0, 1.0], [1.0, 1.0, 1.0], [2.0, 1.0, 1.0]],
|
||||
],
|
||||
dtype=np.float32,
|
||||
)
|
||||
depth = np.ones((2, 3), dtype=np.float32)
|
||||
confidence = np.ones((2, 3), dtype=np.float32)
|
||||
confidence[:, 0] = 0.0
|
||||
|
||||
faces = self.geometry.mesh_triangle_faces(
|
||||
points,
|
||||
depth,
|
||||
confidence,
|
||||
confidence_threshold=0.5,
|
||||
filter_edges=False,
|
||||
max_edge_length=0.0,
|
||||
)
|
||||
|
||||
np.testing.assert_array_equal(faces, [[1, 4, 5], [1, 5, 2]])
|
||||
vertices = points.reshape(-1, 3)[faces[0]]
|
||||
normal = np.cross(
|
||||
vertices[1] - vertices[0],
|
||||
vertices[2] - vertices[0],
|
||||
)
|
||||
self.assertLess(normal[2], 0.0)
|
||||
|
||||
def test_mesh_percentile_reduces_face_count(self):
|
||||
depth = np.ones((3, 3), dtype=np.float32)
|
||||
confidence = np.arange(1, 10, dtype=np.float32).reshape(3, 3)
|
||||
x, y = np.meshgrid(
|
||||
np.arange(3, dtype=np.float32),
|
||||
np.arange(3, dtype=np.float32),
|
||||
)
|
||||
points = np.stack((x, y, depth), axis=-1)
|
||||
face_counts = []
|
||||
for percentile in (0.0, 50.0, 100.0):
|
||||
threshold = self.geometry.confidence_percentile_threshold(
|
||||
depth,
|
||||
confidence,
|
||||
percentile,
|
||||
)
|
||||
faces = self.geometry.mesh_triangle_faces(
|
||||
points,
|
||||
depth,
|
||||
confidence,
|
||||
confidence_threshold=threshold,
|
||||
filter_edges=False,
|
||||
max_edge_length=0.0,
|
||||
)
|
||||
face_counts.append(len(faces))
|
||||
|
||||
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.geometry.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_surface_background_requires_every_vertex_to_be_foreground(self):
|
||||
faces = np.asarray(
|
||||
((0, 1, 4), (0, 4, 3), (1, 2, 5), (1, 5, 4)),
|
||||
dtype=np.int32,
|
||||
)
|
||||
foreground = np.asarray(
|
||||
((True, True, False), (False, True, False)),
|
||||
dtype=bool,
|
||||
)
|
||||
|
||||
filtered = self.geometry.remove_background_faces(faces, foreground)
|
||||
|
||||
np.testing.assert_array_equal(filtered, faces[[0]])
|
||||
|
||||
def test_surface_background_mask_uses_half_alpha_threshold(self):
|
||||
from PIL import Image
|
||||
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
for alpha, expected in (
|
||||
(0, False),
|
||||
(127, False),
|
||||
(128, True),
|
||||
(255, True),
|
||||
):
|
||||
path = root / f"alpha-{alpha}.png"
|
||||
Image.new("RGBA", (2, 2), (10, 20, 30, alpha)).save(path)
|
||||
mask = self.geometry.foreground_mesh_mask(path, (2, 2))
|
||||
self.assertEqual(mask.dtype, np.bool_)
|
||||
self.assertTrue(np.all(mask == expected))
|
||||
|
||||
def test_image_mesh_data_keeps_camera_geometry_and_uv(self):
|
||||
depth = np.full((3, 3), 2.0, dtype=np.float32)
|
||||
confidence = np.ones((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,
|
||||
)
|
||||
frame = self.surface.GeometryFrame(
|
||||
depth=depth,
|
||||
points=self.geometry.unproject_camera_depth(depth, intrinsics),
|
||||
intrinsics=intrinsics,
|
||||
valid_mask=np.ones(depth.shape, dtype=bool),
|
||||
confidence=confidence,
|
||||
)
|
||||
mesh = self.surface.build_surface(frame)
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
result_path = self.surface_outputs.write_surface_archive(
|
||||
mesh,
|
||||
Path(directory) / "mesh.npz",
|
||||
)
|
||||
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["face_confidence"].shape,
|
||||
(8,),
|
||||
)
|
||||
self.assertEqual(
|
||||
set(result.files),
|
||||
{
|
||||
"vertices",
|
||||
"faces",
|
||||
"uv",
|
||||
"intrinsics",
|
||||
"image_size",
|
||||
"reference_depth",
|
||||
"point_depth",
|
||||
"face_confidence",
|
||||
"face_depth_discontinuity",
|
||||
},
|
||||
)
|
||||
np.testing.assert_allclose(result["uv"][0], [0.0, 1.0])
|
||||
np.testing.assert_allclose(result["uv"][-1], [1.0, 0.0])
|
||||
self.assertNotIn("normal_map", result.files)
|
||||
|
||||
def test_moge_surface_uses_the_same_archive_without_normal_data(self):
|
||||
depth = np.full((3, 3), 2.0, 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,
|
||||
)
|
||||
frame = self.surface.GeometryFrame(
|
||||
depth=depth,
|
||||
points=self.geometry.unproject_camera_depth(depth, intrinsics),
|
||||
intrinsics=intrinsics,
|
||||
valid_mask=np.ones(depth.shape, dtype=bool),
|
||||
model_mask=np.asarray(
|
||||
(
|
||||
(False, False, False),
|
||||
(True, True, True),
|
||||
(True, True, True),
|
||||
),
|
||||
dtype=bool,
|
||||
),
|
||||
)
|
||||
mesh = self.surface.build_surface(frame)
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
path = self.surface_outputs.write_surface_archive(
|
||||
mesh,
|
||||
Path(directory) / "mesh.npz",
|
||||
)
|
||||
with np.load(path) as result:
|
||||
self.assertIn("face_mask", result.files)
|
||||
self.assertNotIn("face_confidence", result.files)
|
||||
self.assertNotIn("normal", result.files)
|
||||
self.assertNotIn("normal_map", result.files)
|
||||
|
||||
def test_moge_surface_normal_is_exported_as_a_texture(self):
|
||||
from PIL import Image
|
||||
|
||||
normal = np.asarray(
|
||||
[[[0.0, 0.0, -1.0]]],
|
||||
dtype=np.float32,
|
||||
)
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
path = self.surface_outputs.write_normal_texture(
|
||||
normal,
|
||||
Path(directory) / "normal.png",
|
||||
)
|
||||
with Image.open(path) as image:
|
||||
self.assertEqual(image.mode, "RGB")
|
||||
self.assertEqual(image.size, (1, 1))
|
||||
red, green, blue = image.getpixel((0, 0))
|
||||
self.assertIn(red, {127, 128})
|
||||
self.assertIn(green, {127, 128})
|
||||
self.assertEqual(blue, 255)
|
||||
|
||||
def test_da3_debug_writer_keeps_api_values_without_derived_attributes(self):
|
||||
prediction = SimpleNamespace(
|
||||
depth=np.ones((1, 2, 3), dtype=np.float32),
|
||||
|
||||
Reference in New Issue
Block a user