feat(cutout_tool): Implement cutout region to shape functionality
- Added interaction helpers for cutout region validation and shape menu interaction. - Created main operator for converting image regions into cutout shapes, including AI artifact generation. - Introduced object creation logic for cutout shapes, handling depth and normal textures. - Defined cutout shape identifiers and node groups for various shape types. - Implemented depth texture writing and prediction artifact generation for MoGe-2. - Developed server jobs for generating cutout artifacts, depth planes, and refining image regions. - Added region refinement logic to enhance gesture masks using BEN2.
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import json
|
||||
import builtins
|
||||
import sys
|
||||
from pathlib import Path
|
||||
@@ -9,53 +10,53 @@ import numpy as np
|
||||
SOURCE_ROOT = Path(__file__).parents[1] / "src"
|
||||
sys.path.insert(0, str(SOURCE_ROOT))
|
||||
|
||||
from anyimage.operators.cutout_tool import region_to_shape as mesh
|
||||
from anyimage.operators.cutout_tool import region_to_shape as cutout_nodes
|
||||
from anyimage.operators.cutout_tool import region_to_shape as shapes
|
||||
from anyimage.operators.cutout_tool.geometry import balloon as cutout_profile
|
||||
from anyimage.operators.cutout_tool.geometry import depth_calibration as cutout_depth
|
||||
from anyimage.operators.cutout_tool.geometry.balloon import poisson_balloon_profile
|
||||
from anyimage.operators.common.mesh_result import ImageGeometry, load_image_geometry
|
||||
from anyimage.operators.common.image_region import (
|
||||
RegionMask,
|
||||
RegionPath,
|
||||
rasterize_region_path,
|
||||
)
|
||||
from anyimage.operators.convert_to_plane.normal_map import rasterize_tangent_normal
|
||||
from anyimage.operators.cutout_tool.geometry.region_contours import (
|
||||
from anyimage.operators.common.depth_data import (
|
||||
depth_uniform_scale,
|
||||
load_depth_metadata,
|
||||
)
|
||||
from anyimage.operators.cutout_tool import geometry as cutout_depth
|
||||
from anyimage.operators.cutout_tool import geometry as cutout_profile
|
||||
from anyimage.operators.cutout_tool import geometry as mesh
|
||||
from anyimage.operators.cutout_tool import object as cutout_nodes
|
||||
from anyimage.operators.cutout_tool import shape as shapes
|
||||
from anyimage.operators.cutout_tool.geometry import (
|
||||
RegionContours,
|
||||
extract_region_contours,
|
||||
poisson_balloon_profile,
|
||||
)
|
||||
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
|
||||
assert mesh.limit_cutout_mesh_resolution(256, (4096, 1024), 2048) == 128
|
||||
assert mesh.limit_cutout_mesh_resolution(256, (1024, 512), 2048) == 256
|
||||
assert mesh.limit_cutout_mesh_resolution(8, (8192, 4096), 2048) == 4
|
||||
|
||||
|
||||
def test_image_geometry_loader_uses_one_schema_for_plane_and_cutout(tmp_path):
|
||||
path = tmp_path / "image_geometry.npz"
|
||||
points = np.ones((2, 3, 3), dtype=np.float32)
|
||||
valid = np.ones((2, 3), dtype=bool)
|
||||
np.savez_compressed(
|
||||
path,
|
||||
points=points,
|
||||
intrinsics=np.eye(3, dtype=np.float32),
|
||||
valid=valid,
|
||||
calibration_valid=valid,
|
||||
image_size=np.asarray((3, 2), dtype=np.int32),
|
||||
reference_depth=np.asarray(1.0, dtype=np.float32),
|
||||
region_transparent_fraction=np.asarray(0.25, dtype=np.float32),
|
||||
def test_depth_metadata_loader_uses_one_schema_for_plane_and_cutout(tmp_path):
|
||||
path = tmp_path / "depth.json"
|
||||
path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"image_size": [3, 2],
|
||||
"intrinsics": np.eye(3).tolist(),
|
||||
"reference_depth": 1.0,
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
field = load_image_geometry(path)
|
||||
metadata = load_depth_metadata(path)
|
||||
|
||||
assert metadata["image_size"] == (3, 2)
|
||||
assert metadata["intrinsics"] == ((1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 1.0))
|
||||
assert metadata["reference_depth"] == 1.0
|
||||
|
||||
assert field.points.shape == (2, 3, 3)
|
||||
assert field.image_size == (3, 2)
|
||||
assert field.reference_depth == 1.0
|
||||
assert field.region_transparent_fraction == 0.25
|
||||
|
||||
def test_cutout_node_groups_are_loaded_from_the_asset():
|
||||
loaded = object()
|
||||
@@ -63,7 +64,7 @@ def test_cutout_node_groups_are_loaded_from_the_asset():
|
||||
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
|
||||
assert cutout_nodes.load_cutout_node_group() is loaded
|
||||
load.assert_called_with(shapes.CUTOUT_NODE_GROUP_NAME)
|
||||
assert load.call_count == 4
|
||||
|
||||
@@ -81,9 +82,7 @@ def test_base_plane_fit_is_stable_for_a_slender_diagonal_domain():
|
||||
|
||||
plane = cutout_depth._stable_plane(coordinates, values)
|
||||
selected_plane = coordinates @ plane[:2] + plane[2]
|
||||
corners = np.asarray(
|
||||
((-1.0, -1.0), (-1.0, 1.0), (1.0, -1.0), (1.0, 1.0))
|
||||
)
|
||||
corners = np.asarray(((-1.0, -1.0), (-1.0, 1.0), (1.0, -1.0), (1.0, 1.0)))
|
||||
corner_plane = corners @ plane[:2] + plane[2]
|
||||
|
||||
assert np.ptp(corner_plane) <= np.ptp(selected_plane) * 1.25
|
||||
@@ -166,63 +165,29 @@ def mesh_component_count(faces):
|
||||
return count
|
||||
|
||||
|
||||
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)),))
|
||||
triangle_normal = np.asarray((((1.0, 0.0, 0.0),) * 3,))
|
||||
triangle_tangent = np.asarray((((0.0, 1.0, 0.0),) * 3,))
|
||||
triangle_sign = np.ones((1, 3), dtype=np.float32)
|
||||
|
||||
tangent = rasterize_tangent_normal(
|
||||
object_normal,
|
||||
triangle_uv,
|
||||
triangle_normal,
|
||||
triangle_tangent,
|
||||
triangle_sign,
|
||||
)
|
||||
|
||||
assert np.allclose(tangent[0, 0], (0.0, 1.0, 0.0))
|
||||
assert not np.allclose(tangent[0, 0], object_normal[0, 0])
|
||||
assert np.allclose(tangent[2, 2], (0.0, 0.0, 1.0))
|
||||
|
||||
|
||||
def test_depth_surface_discontinuity_map_uses_local_three_by_three_range():
|
||||
depth = np.ones((5, 5), dtype=np.float32)
|
||||
depth[:, 3:] = 2.0
|
||||
valid = np.ones_like(depth, dtype=bool)
|
||||
|
||||
discontinuity = mesh._depth_discontinuity_map(depth, valid)
|
||||
|
||||
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 = ImageGeometry(
|
||||
points=points,
|
||||
intrinsics=np.asarray(
|
||||
((100.0, 0.0, 50.0), (0.0, 100.0, 25.0), (0.0, 0.0, 1.0))
|
||||
metadata = {
|
||||
"intrinsics": (
|
||||
(100.0, 0.0, 50.0),
|
||||
(0.0, 100.0, 25.0),
|
||||
(0.0, 0.0, 1.0),
|
||||
),
|
||||
valid=np.ones((51, 101), dtype=bool),
|
||||
calibration_valid=np.ones((51, 101), dtype=bool),
|
||||
image_size=(101, 51),
|
||||
reference_depth=2.0,
|
||||
region_transparent_fraction=0.0,
|
||||
)
|
||||
"image_size": (101, 51),
|
||||
"reference_depth": 2.0,
|
||||
}
|
||||
|
||||
uniform_scale = mesh._depth_uniform_scale(
|
||||
uniform_scale = depth_uniform_scale(
|
||||
SimpleNamespace(),
|
||||
field,
|
||||
metadata,
|
||||
bounds=(-2.0, 2.0, -1.0, 1.0),
|
||||
)
|
||||
|
||||
assert uniform_scale == 2.0
|
||||
reference_depth = cutout_depth.metric_depth_reference(
|
||||
points[..., 2],
|
||||
field.calibration_valid,
|
||||
np.ones((51, 101), dtype=bool),
|
||||
uniform_scale,
|
||||
)
|
||||
assert np.isclose(reference_depth, 5.8, atol=1e-6)
|
||||
@@ -245,6 +210,21 @@ def test_moge_surface_field_uses_the_model_validity_mask():
|
||||
assert np.array_equal(frame.valid_mask, prediction.mask[0])
|
||||
|
||||
|
||||
def test_moge_z_depth_frame_does_not_require_points():
|
||||
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.ones((1, 2, 2), dtype=bool),
|
||||
intrinsics=np.eye(3, dtype=np.float32)[None],
|
||||
points=None,
|
||||
)
|
||||
|
||||
frame = moge2_model.geometry_frame(prediction)
|
||||
|
||||
assert frame.points is None
|
||||
assert frame.valid_mask.all()
|
||||
|
||||
|
||||
def test_region_contours_group_holes_with_their_connected_component():
|
||||
alpha = np.zeros((120, 200), dtype=np.uint8)
|
||||
alpha[20:100, 50:160] = 255
|
||||
@@ -259,13 +239,10 @@ def test_region_contours_group_holes_with_their_connected_component():
|
||||
assert region_contours.image_size == (200, 120)
|
||||
assert sorted(len(component) for component in region_contours.components) == [1, 2]
|
||||
contours = [
|
||||
contour
|
||||
for component in region_contours.components
|
||||
for contour in component
|
||||
contour for component in region_contours.components for contour in component
|
||||
]
|
||||
bounds = [
|
||||
(*np.min(contour, axis=0), *np.max(contour, axis=0))
|
||||
for contour in contours
|
||||
(*np.min(contour, axis=0), *np.max(contour, axis=0)) for contour in contours
|
||||
]
|
||||
assert (50.0, 20.0, 160.0, 100.0) in bounds
|
||||
assert (80.0, 45.0, 120.0, 75.0) in bounds
|
||||
@@ -294,7 +271,7 @@ def test_cutout_crop_bounds_map_to_the_source_plane():
|
||||
empty_image_offset=(-0.5, -0.5),
|
||||
)
|
||||
|
||||
bounds = mesh._crop_plane_bounds(
|
||||
bounds = mesh.crop_plane_bounds(
|
||||
source,
|
||||
(50, 25, 150, 75),
|
||||
(200, 100),
|
||||
@@ -308,7 +285,7 @@ def test_depth_surface_cutout_uses_half_alpha_boundary():
|
||||
alpha[2:10, 2:14] = 0.49
|
||||
alpha[4:8, 5:11] = 0.51
|
||||
|
||||
region_contours = mesh._extract_cutout_contours(
|
||||
region_contours = mesh.extract_cutout_contours(
|
||||
RegionMask(alpha, (0, 0, 16, 12)),
|
||||
64,
|
||||
threshold=0.5,
|
||||
@@ -319,6 +296,7 @@ def test_depth_surface_cutout_uses_half_alpha_boundary():
|
||||
np.testing.assert_allclose(contour.min(axis=0), (5.0, 4.0))
|
||||
np.testing.assert_allclose(contour.max(axis=0), (11.0, 8.0))
|
||||
|
||||
|
||||
def test_blender_cutout_alpha_pipeline_does_not_import_pillow():
|
||||
original_import = builtins.__import__
|
||||
|
||||
@@ -343,6 +321,7 @@ def test_blender_cutout_alpha_pipeline_does_not_import_pillow():
|
||||
assert region_contours.components
|
||||
assert np.any((antialiased > 0.0) & (antialiased < 1.0))
|
||||
|
||||
|
||||
def test_blender_cutout_contour_resolution_tracks_mesh_detail():
|
||||
expected = RegionContours(
|
||||
components=(((((0, 0), (1, 0), (0, 1))),),),
|
||||
@@ -350,37 +329,38 @@ def test_blender_cutout_contour_resolution_tracks_mesh_detail():
|
||||
)
|
||||
region_mask = RegionMask(np.ones((2, 2), dtype=np.float32), (0, 0, 2, 2))
|
||||
with patch.object(mesh, "extract_region_contours", return_value=expected) as trace:
|
||||
assert mesh._extract_cutout_contours(region_mask, 32) == expected
|
||||
assert mesh._extract_cutout_contours(region_mask, 256) == expected
|
||||
assert mesh.extract_cutout_contours(region_mask, 32) == expected
|
||||
assert mesh.extract_cutout_contours(region_mask, 256) == expected
|
||||
|
||||
assert trace.call_args_list[0].kwargs["max_resolution"] == 512
|
||||
assert trace.call_args_list[1].kwargs["max_resolution"] == 1024
|
||||
|
||||
|
||||
def test_cutout_point_spacing_uses_the_complete_source_plane():
|
||||
source = SimpleNamespace(matrix_world=object())
|
||||
with (
|
||||
patch.object(mesh, "image_empty_bounds", return_value=(-2.0, 2.0, -1.0, 1.0)),
|
||||
patch.object(
|
||||
mesh,
|
||||
"_world_plane_metric",
|
||||
"world_plane_metric",
|
||||
return_value=(((1.0, 0.0), (0.0, 1.0)), 1.0),
|
||||
),
|
||||
):
|
||||
spacing = mesh._cutout_point_spacing(source, 200)
|
||||
spacing = mesh.cutout_point_spacing(source, 200)
|
||||
|
||||
assert spacing == 0.02
|
||||
|
||||
|
||||
def test_cutout_cleanup_value_scales_the_ui_without_changing_node_area():
|
||||
assert np.isclose(
|
||||
mesh._cutout_cleanup_value(
|
||||
mesh.cutout_cleanup_value(
|
||||
0.25,
|
||||
((1.0, 0.0), (0.0, 1.0)),
|
||||
),
|
||||
6.25,
|
||||
)
|
||||
assert np.isclose(
|
||||
mesh._cutout_cleanup_value(
|
||||
mesh.cutout_cleanup_value(
|
||||
0.25,
|
||||
((4.0, 0.0), (0.0, 9.0)),
|
||||
),
|
||||
@@ -437,6 +417,8 @@ def test_poisson_balloon_profile_makes_wide_regions_thicker():
|
||||
narrow_profile = poisson_balloon_profile(narrow, faces, boundary)
|
||||
|
||||
assert wide_profile[4] > narrow_profile[4]
|
||||
|
||||
|
||||
def test_teddy_balloon_profile_uses_chordal_axis_radius():
|
||||
points = np.asarray(
|
||||
((0, 0), (4, 0), (4, 2), (0, 2), (2, 1)),
|
||||
@@ -463,14 +445,10 @@ def test_teddy_balloon_profile_uses_chordal_axis_radius():
|
||||
assert np.isclose(profile[4], 0.45, atol=1e-6)
|
||||
|
||||
|
||||
|
||||
|
||||
def test_cutout_domain_adapts_boundary_spacing_for_a_small_valid_region():
|
||||
regions = (
|
||||
(((2046, 2046), (2050, 2046), (2050, 2050), (2046, 2050)),),
|
||||
)
|
||||
regions = ((((2046, 2046), (2050, 2046), (2050, 2050), (2046, 2050)),),)
|
||||
|
||||
domain = mesh._cutout_domain(
|
||||
domain = mesh.cutout_domain(
|
||||
RegionContours(regions, (4096, 4096)),
|
||||
(-1.0, 1.0, -1.0, 1.0),
|
||||
0.5,
|
||||
@@ -484,7 +462,7 @@ def test_cutout_domain_adapts_boundary_spacing_for_a_small_valid_region():
|
||||
def test_cutout_domain_builds_a_full_image_at_minimum_detail():
|
||||
regions = ((((0, 0), (4096, 0), (4096, 4096), (0, 4096)),),)
|
||||
|
||||
domain = mesh._cutout_domain(
|
||||
domain = mesh.cutout_domain(
|
||||
RegionContours(regions, (4096, 4096)),
|
||||
(-1.0, 1.0, -1.0, 1.0),
|
||||
0.5,
|
||||
@@ -494,10 +472,11 @@ def test_cutout_domain_builds_a_full_image_at_minimum_detail():
|
||||
assert len(domain[0]) >= 4
|
||||
assert len(domain[1]) >= 2
|
||||
|
||||
|
||||
def test_cutout_domain_centers_vertices_on_crop_bounds():
|
||||
regions = ((((0, 0), (100, 0), (100, 50), (0, 50)),),)
|
||||
|
||||
vertices, _faces, uv, _boundary, _planar, _profile = mesh._cutout_domain(
|
||||
vertices, _faces, uv, _boundary, _planar, _profile = mesh.cutout_domain(
|
||||
RegionContours(regions, (100, 50)),
|
||||
(2.0, 6.0, -3.0, 1.0),
|
||||
0.5,
|
||||
@@ -516,11 +495,9 @@ def test_cutout_domain_keeps_overlapping_regions_disconnected():
|
||||
((20, 0), (100, 0), (100, 80), (20, 80)),
|
||||
((50, 20), (50, 60), (70, 60), (70, 20)),
|
||||
),
|
||||
(
|
||||
((0, 20), (40, 20), (40, 50), (0, 50)),
|
||||
),
|
||||
(((0, 20), (40, 20), (40, 50), (0, 50)),),
|
||||
)
|
||||
domain = mesh._cutout_domain(
|
||||
domain = mesh.cutout_domain(
|
||||
RegionContours(regions, (120, 80)),
|
||||
(0.0, 1.0, 0.0, 1.0),
|
||||
0.025,
|
||||
@@ -544,3 +521,15 @@ def test_cutout_domain_keeps_overlapping_regions_disconnected():
|
||||
assert not in_hole.any()
|
||||
assert in_left_region.any()
|
||||
assert mesh_component_count(domain[1]) == 2
|
||||
|
||||
|
||||
def test_region_calibration_uses_the_final_region_mask():
|
||||
region_mask = RegionMask(
|
||||
np.asarray(((1.0, 1.0), (1.0, 0.0)), dtype=np.float32),
|
||||
(0, 0, 2, 2),
|
||||
)
|
||||
|
||||
valid, transparent_fraction = mesh.region_calibration(region_mask, (2, 2))
|
||||
|
||||
assert np.array_equal(valid, ((True, True), (True, False)))
|
||||
assert transparent_fraction == 0.25
|
||||
|
||||
Reference in New Issue
Block a user