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:
@@ -11,10 +11,31 @@ from PIL import Image
|
||||
SOURCE_ROOT = Path(__file__).parents[1] / "src"
|
||||
sys.path.insert(0, str(SOURCE_ROOT))
|
||||
|
||||
from anyimage.operators import cutout_depth, cutout_teddy, mesh
|
||||
from anyimage.operators.cutout_profile import poisson_balloon_profile
|
||||
from anyimage.operators.normal_map import rasterize_tangent_normal
|
||||
from anyimage.server.geometry.cutout import alpha_regions, image_alpha_regions
|
||||
from anyimage.operators.cutout_tool import balloon_profile as cutout_profile
|
||||
from anyimage.operators.cutout_tool import build_shapes as mesh
|
||||
from anyimage.operators.cutout_tool import depth_calibration as cutout_depth
|
||||
from anyimage.operators.cutout_tool import node_assets as cutout_nodes
|
||||
from anyimage.operators.cutout_tool import shape_types as shapes
|
||||
from anyimage.operators.cutout_tool.balloon_profile import poisson_balloon_profile
|
||||
from anyimage.operators.common.normal_map import rasterize_tangent_normal
|
||||
from anyimage.operators.cutout_tool.mask_regions import alpha_regions, image_alpha_regions
|
||||
from anyimage.server.models import moge2 as moge2_model
|
||||
|
||||
|
||||
def test_cutout_mesh_detail_uses_the_cropped_analysis_scale():
|
||||
assert mesh.limited_cutout_mesh_detail(256, (4096, 1024), 2048) == 128
|
||||
assert mesh.limited_cutout_mesh_detail(256, (1024, 512), 2048) == 256
|
||||
assert mesh.limited_cutout_mesh_detail(8, (8192, 4096), 2048) == 4
|
||||
|
||||
def test_cutout_node_groups_are_loaded_from_the_asset():
|
||||
loaded = object()
|
||||
with patch.object(
|
||||
cutout_nodes, "load_asset_node_group", return_value=loaded
|
||||
) as load:
|
||||
for shape in shapes.CUTOUT_NODE_GROUP_NAMES:
|
||||
assert cutout_nodes.cutout_node_group(shape) is loaded
|
||||
load.assert_called_with(shapes.CUTOUT_NODE_GROUP_NAME)
|
||||
assert load.call_count == 4
|
||||
|
||||
|
||||
def test_base_plane_fit_is_stable_for_a_slender_diagonal_domain():
|
||||
@@ -40,36 +61,58 @@ def test_base_plane_fit_is_stable_for_a_slender_diagonal_domain():
|
||||
|
||||
|
||||
def test_depth_balloon_plane_calibration_matches_cutout_axes():
|
||||
vertices = np.asarray(
|
||||
(
|
||||
(-1.0, -1.0, 0.0),
|
||||
(1.0, -1.0, 0.0),
|
||||
(1.0, 1.0, 0.0),
|
||||
(-1.0, 1.0, 0.0),
|
||||
),
|
||||
dtype=np.float32,
|
||||
)
|
||||
uv = np.asarray(
|
||||
((0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)),
|
||||
dtype=np.float32,
|
||||
)
|
||||
depth_data = {
|
||||
"depth_signal": np.zeros((2, 2), dtype=np.float32),
|
||||
"baseline_vector": np.asarray((0.4, -0.2, 0.1), dtype=np.float32),
|
||||
"valid": np.ones((2, 2), dtype=bool),
|
||||
"crop_size": (2, 2),
|
||||
}
|
||||
vertices = []
|
||||
uv = []
|
||||
camera_depth = np.empty((3, 3), dtype=np.float32)
|
||||
for row in range(3):
|
||||
local_z = 1.0 - row
|
||||
for column in range(3):
|
||||
local_y = column - 1.0
|
||||
vertices.append((0.0, local_y, local_z))
|
||||
uv.append((column * 0.5, 1.0 - row * 0.5))
|
||||
camera_depth[row, column] = 2.0 + 0.2 * local_y + 0.1 * local_z
|
||||
|
||||
_depth, local_plane = cutout_depth.depth_balloon_fields(
|
||||
reference, direction = cutout_depth.depth_balloon_projection_parameters(
|
||||
vertices,
|
||||
uv,
|
||||
depth_data,
|
||||
camera_depth,
|
||||
np.ones((3, 3), dtype=bool),
|
||||
0.5,
|
||||
)
|
||||
expected_direction = np.asarray((-0.1, 0.05, 1.0), dtype=np.float64)
|
||||
expected_direction /= np.linalg.norm(expected_direction)
|
||||
|
||||
assert np.isclose(
|
||||
reference,
|
||||
np.percentile(camera_depth, 95.0) * 0.5,
|
||||
atol=1e-6,
|
||||
)
|
||||
assert np.allclose(direction, expected_direction, atol=1e-6)
|
||||
|
||||
|
||||
def test_depth_balloon_reference_uses_the_shared_upper_depth_percentile():
|
||||
size = 10
|
||||
row, column = np.indices((size, size))
|
||||
camera_depth = np.full((size, size), 2.0, dtype=np.float32)
|
||||
camera_depth[(row + column) % 2 == 0] = 2.2
|
||||
vertices = []
|
||||
uv = []
|
||||
for y in range(size):
|
||||
for x in range(size):
|
||||
vertices.append((0.0, float(x), float(size - 1 - y)))
|
||||
uv.append((x / (size - 1), 1.0 - y / (size - 1)))
|
||||
|
||||
reference, direction = cutout_depth.depth_balloon_projection_parameters(
|
||||
vertices,
|
||||
uv,
|
||||
camera_depth,
|
||||
np.ones((size, size), dtype=bool),
|
||||
1.0,
|
||||
)
|
||||
oriented = cutout_depth.orient_depth_balloon_plane(local_plane)
|
||||
|
||||
assert np.allclose(local_plane, (0.2, 0.1, 0.05), atol=1e-6)
|
||||
assert np.allclose(oriented, (-0.2, 0.1, 0.05), atol=1e-6)
|
||||
assert reference > float(np.median(camera_depth))
|
||||
assert np.isclose(reference, 2.2, atol=1e-6)
|
||||
assert np.allclose(direction, (0.0, 0.0, 1.0), atol=1e-6)
|
||||
|
||||
|
||||
def mesh_component_count(faces):
|
||||
@@ -93,7 +136,7 @@ def mesh_component_count(faces):
|
||||
return count
|
||||
|
||||
|
||||
def test_tangent_normal_pixels_use_the_final_surface_frame():
|
||||
def test_tangent_normal_pixels_use_the_final_geometry_frame():
|
||||
object_normal = np.zeros((3, 3, 3), dtype=np.float32)
|
||||
object_normal[..., 2] = 1.0
|
||||
triangle_uv = np.asarray((((0.0, 0.0), (1.0, 0.0), (0.0, 1.0)),))
|
||||
@@ -114,15 +157,6 @@ def test_tangent_normal_pixels_use_the_final_surface_frame():
|
||||
assert np.allclose(tangent[2, 2], (0.0, 0.0, 1.0))
|
||||
|
||||
|
||||
def test_plane_source_topology_is_one_quad_at_every_detail_level():
|
||||
low = mesh._plane_domain((-2.0, 2.0, -1.0, 1.0), 1)
|
||||
high = mesh._plane_domain((-2.0, 2.0, -1.0, 1.0), 512)
|
||||
|
||||
assert low[0].shape == high[0].shape == (4, 3)
|
||||
assert low[1].shape == high[1].shape == (1, 4)
|
||||
|
||||
|
||||
|
||||
def test_depth_surface_discontinuity_map_uses_local_three_by_three_range():
|
||||
depth = np.ones((5, 5), dtype=np.float32)
|
||||
depth[:, 3:] = 2.0
|
||||
@@ -130,8 +164,52 @@ def test_depth_surface_discontinuity_map_uses_local_three_by_three_range():
|
||||
|
||||
discontinuity = mesh._depth_discontinuity_map(depth, valid)
|
||||
|
||||
assert discontinuity[2, 0] < mesh.DEPTH_EDGE_THRESHOLD
|
||||
assert discontinuity[2, 2] > mesh.DEPTH_EDGE_THRESHOLD
|
||||
assert discontinuity[2, 0] == 0.0
|
||||
assert discontinuity[2, 2] == 1.0
|
||||
|
||||
|
||||
def test_depth_surface_projection_uses_one_scale_for_all_camera_axes():
|
||||
points = np.zeros((51, 101, 3), dtype=np.float32)
|
||||
points[..., 2] = np.linspace(1.0, 3.0, 51 * 101).reshape(51, 101)
|
||||
field = (
|
||||
points,
|
||||
np.asarray(((100.0, 0.0, 50.0), (0.0, 100.0, 25.0), (0.0, 0.0, 1.0))),
|
||||
np.ones((51, 101), dtype=bool),
|
||||
np.ones((51, 101), dtype=bool),
|
||||
(101, 51),
|
||||
2.0,
|
||||
)
|
||||
|
||||
uniform_scale = mesh._surface_uniform_scale(
|
||||
SimpleNamespace(),
|
||||
field,
|
||||
bounds=(-2.0, 2.0, -1.0, 1.0),
|
||||
)
|
||||
|
||||
assert uniform_scale == 2.0
|
||||
reference_depth = cutout_depth.metric_depth_reference(
|
||||
points[..., 2],
|
||||
field[3],
|
||||
uniform_scale,
|
||||
)
|
||||
assert np.isclose(reference_depth, 5.8, atol=1e-6)
|
||||
|
||||
|
||||
def test_moge_surface_field_uses_the_model_validity_mask():
|
||||
prediction = moge2_model.Prediction(
|
||||
depth=np.ones((1, 2, 2), dtype=np.float32),
|
||||
normal=np.zeros((1, 2, 2, 3), dtype=np.float32),
|
||||
mask=np.asarray((((True, False), (True, True)),)),
|
||||
intrinsics=np.asarray(
|
||||
(((1.0, 0.0, 0.5), (0.0, 1.0, 0.5), (0.0, 0.0, 1.0)),),
|
||||
dtype=np.float32,
|
||||
),
|
||||
points=np.ones((1, 2, 2, 3), dtype=np.float32),
|
||||
)
|
||||
|
||||
frame = moge2_model.geometry_frame(prediction)
|
||||
|
||||
assert np.array_equal(frame.valid_mask, prediction.mask[0])
|
||||
|
||||
|
||||
def test_cutout_regions_group_holes_with_their_connected_component():
|
||||
@@ -215,7 +293,19 @@ def test_blender_cutout_alpha_pipeline_does_not_import_pillow():
|
||||
assert regions
|
||||
assert np.any((antialiased > 0.0) & (antialiased < 1.0))
|
||||
|
||||
def test_cutout_geometry_ignores_source_alpha_without_refine():
|
||||
def test_unrefined_cutout_geometry_uses_strict_tenth_source_alpha_threshold():
|
||||
alpha = np.full((40, 60), 0.1, dtype=np.float32)
|
||||
alpha[10:30, 20:40] = 0.11
|
||||
polygon = ((0, 0), (60, 0), (60, 40), (0, 40))
|
||||
|
||||
regions, size = mesh._geometry_cutout_regions(alpha, polygon, 64)
|
||||
contour = np.asarray(regions[0][0])
|
||||
|
||||
assert size == (60, 40)
|
||||
np.testing.assert_allclose(contour.min(axis=0), (20.0, 10.0))
|
||||
np.testing.assert_allclose(contour.max(axis=0), (40.0, 30.0))
|
||||
|
||||
def test_cutout_geometry_excludes_zero_source_alpha_without_refine():
|
||||
alpha = np.zeros((80, 120), dtype=np.float32)
|
||||
alpha[10:70, 10:110] = 1.0
|
||||
alpha[30:50, 45:75] = 0.0
|
||||
@@ -225,7 +315,7 @@ def test_cutout_geometry_ignores_source_alpha_without_refine():
|
||||
|
||||
assert size == (120, 80)
|
||||
assert len(regions) == 1
|
||||
assert len(regions[0]) == 1
|
||||
assert len(regions[0]) == 2
|
||||
|
||||
|
||||
def test_cutout_geometry_uses_final_alpha_with_refine():
|
||||
@@ -242,7 +332,7 @@ def test_cutout_geometry_uses_final_alpha_with_refine():
|
||||
assert len(regions) == 1
|
||||
assert len(regions[0]) == 2
|
||||
|
||||
def test_cutout_geometry_can_extend_beyond_the_source_image():
|
||||
def test_unrefined_cutout_geometry_discards_out_of_frame_selection():
|
||||
alpha = np.zeros((60, 100), dtype=np.float32)
|
||||
alpha[10:50] = 1.0
|
||||
polygon = ((-20, 10), (120, 10), (120, 50), (-20, 50))
|
||||
@@ -253,8 +343,8 @@ def test_cutout_geometry_can_extend_beyond_the_source_image():
|
||||
[np.asarray(contour) for region in regions for contour in region]
|
||||
)
|
||||
assert size == (100, 60)
|
||||
assert points[:, 0].min() <= -19.0
|
||||
assert points[:, 0].max() >= 119.0
|
||||
assert points[:, 0].min() >= 0.0
|
||||
assert points[:, 0].max() <= 100.0
|
||||
|
||||
def test_refined_cutout_geometry_discards_out_of_frame_selection():
|
||||
alpha = np.zeros((60, 100), dtype=np.float32)
|
||||
@@ -297,6 +387,23 @@ def test_cutout_point_spacing_uses_the_complete_source_plane():
|
||||
assert spacing == 0.02
|
||||
|
||||
|
||||
def test_cutout_cleanup_area_uses_point_spacing_and_plane_area_scale():
|
||||
assert np.isclose(
|
||||
mesh._cutout_cleanup_area(
|
||||
0.25,
|
||||
((1.0, 0.0), (0.0, 1.0)),
|
||||
),
|
||||
0.0625,
|
||||
)
|
||||
assert np.isclose(
|
||||
mesh._cutout_cleanup_area(
|
||||
0.25,
|
||||
((4.0, 0.0), (0.0, 9.0)),
|
||||
),
|
||||
0.0625 / 6.0,
|
||||
)
|
||||
|
||||
|
||||
def test_poisson_balloon_profile_is_zero_on_boundary_and_positive_inside():
|
||||
points = np.asarray(
|
||||
((0, 0), (2, 0), (2, 2), (0, 2), (1, 1)),
|
||||
@@ -351,11 +458,11 @@ def test_teddy_balloon_profile_uses_chordal_axis_radius():
|
||||
nodes = np.asarray(((1, 1), (3, 1)), dtype=float)
|
||||
|
||||
with patch.object(
|
||||
cutout_teddy,
|
||||
cutout_profile,
|
||||
"chordal_axis",
|
||||
return_value=(nodes, [(0, 1)], np.asarray((1.0, 1.0))),
|
||||
):
|
||||
profile = cutout_teddy.teddy_balloon_heights(
|
||||
profile = cutout_profile.teddy_balloon_heights(
|
||||
points,
|
||||
faces,
|
||||
points[:4],
|
||||
|
||||
Reference in New Issue
Block a user