Add cutout profile and teddy balloon height calculations

- Implemented cotangent Poisson field and Poisson balloon profile in cutout_profile.py.
- Added teddy balloon heights calculation and related functions in cutout_teddy.py.
- Introduced interaction utilities for image editing in interaction.py.
- Developed depth normalization and analysis functions in cutout_depth.py.
- Created a validation script for cutout assets in validate_cutout_asset.py.
This commit is contained in:
2026-08-27 22:46:57 +08:00
parent 6c8358f92f
commit 7d39fe8c5e
40 changed files with 3058 additions and 3896 deletions

View File

@@ -11,11 +11,67 @@ from PIL import Image
SOURCE_ROOT = Path(__file__).parents[1] / "src"
sys.path.insert(0, str(SOURCE_ROOT))
from anyimage.operators import mesh
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
def test_base_plane_fit_is_stable_for_a_slender_diagonal_domain():
along = np.linspace(-1.0, 1.0, 101)
across = np.resize(np.asarray((-0.01, 0.0, 0.01)), len(along))
coordinates = np.column_stack(
(
along - across,
along + across,
)
)
values = 0.4 * along + np.sign(across) * 0.05
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))
)
corner_plane = corners @ plane[:2] + plane[2]
assert np.ptp(corner_plane) <= np.ptp(selected_plane) * 1.25
assert np.corrcoef(selected_plane, along)[0, 1] > 0.99
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),
}
_depth, local_plane = cutout_depth.depth_balloon_fields(
vertices,
uv,
depth_data,
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)
def mesh_component_count(faces):
adjacency = {}
for face in faces:
@@ -66,63 +122,6 @@ def test_plane_source_topology_is_one_quad_at_every_detail_level():
assert low[1].shape == high[1].shape == (1, 4)
def test_cutout_front_back_and_side_faces_point_outward():
vertices = np.asarray(
((0.0, 0.0, 0.0), (0.0, 1.0, 0.0), (1.0, 0.0, 0.0)),
dtype=np.float64,
)
faces = np.asarray(((0, 1, 2),), dtype=np.int32)
uv = vertices[:, :2].astype(np.float32)
boundary = np.ones(3, dtype=bool)
layered = mesh._layered_geometry(
(vertices, faces, uv, boundary, vertices[:, :2]),
mesh.CUTOUT,
)
positioned = layered[0] + layered[3]
output_faces = layered[1]
def face_normal(face):
points = positioned[np.asarray(face[:3], dtype=np.int32)]
return np.cross(points[1] - points[0], points[2] - points[0])
assert face_normal(output_faces[0])[2] > 0.0
assert face_normal(output_faces[1])[2] < 0.0
center = vertices[:, :2].mean(axis=0)
for face in output_faces[2:]:
normal = face_normal(face)
side_center = positioned[np.asarray(face, dtype=np.int32), :2].mean(axis=0)
assert np.dot(normal[:2], side_center - center) > 0.0
def test_depth_surface_moves_both_sides_with_camera_projection_and_extrudes_back():
domain = mesh._plane_domain((-1.0, 1.0, -1.0, 1.0), 4)
field = np.zeros((2, 2, 3), dtype=np.float32)
field[..., 2] = 1.0
valid = np.ones((2, 2), dtype=bool)
camera_depth = np.ones((2, 2), dtype=np.float32)
depth_surface = mesh._layered_geometry(
domain, mesh.DEPTH_SURFACE, (field, valid, camera_depth)
)
base_count = len(domain[0])
assert np.allclose(depth_surface[4][:base_count, 2], 1.0)
assert np.allclose(depth_surface[4][base_count:, 2], 1.0)
assert np.allclose(depth_surface[3][:base_count], 0.0)
assert np.allclose(depth_surface[3][base_count:, 2], -1.0)
assert all(depth_surface[5][: len(domain[1])])
assert not any(depth_surface[5][len(domain[1]) :])
def test_depth_surface_records_depth_discontinuities_for_geometry_nodes():
faces = np.asarray(((0, 1, 2), (1, 2, 3)), dtype=np.int32)
depth = np.asarray((1.0, 1.01, 1.02, 2.0), dtype=np.float32)
discontinuity = mesh._depth_discontinuity_values(faces, depth)
assert discontinuity[0] < mesh.DEPTH_EDGE_THRESHOLD
assert discontinuity[1] > mesh.DEPTH_EDGE_THRESHOLD
def test_depth_surface_discontinuity_map_uses_local_three_by_three_range():
depth = np.ones((5, 5), dtype=np.float32)
@@ -298,6 +297,78 @@ def test_cutout_point_spacing_uses_the_complete_source_plane():
assert spacing == 0.02
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)),
dtype=float,
)
faces = ((0, 1, 4), (1, 2, 4), (2, 3, 4), (3, 0, 4))
boundary = np.asarray((True, True, True, True, False))
profile = poisson_balloon_profile(points, faces, boundary)
assert np.all(profile[:4] == 0.0)
assert profile[4] > 0.0
def test_poisson_balloon_profile_scales_in_world_units():
points = np.asarray(
((0, 0), (2, 0), (2, 2), (0, 2), (1, 1)),
dtype=float,
)
faces = ((0, 1, 4), (1, 2, 4), (2, 3, 4), (3, 0, 4))
boundary = np.asarray((True, True, True, True, False))
original = poisson_balloon_profile(points, faces, boundary)
scaled = poisson_balloon_profile(points * 2.0, faces, boundary)
assert np.isclose(scaled[4], original[4] * 2.0, atol=1e-6)
def test_poisson_balloon_profile_makes_wide_regions_thicker():
faces = ((0, 1, 4), (1, 2, 4), (2, 3, 4), (3, 0, 4))
boundary = np.asarray((True, True, True, True, False))
wide = np.asarray(
((0, 0), (4, 0), (4, 2), (0, 2), (2, 1)),
dtype=float,
)
narrow = np.asarray(
((0, 0), (4, 0), (4, 0.6), (0, 0.6), (2, 0.3)),
dtype=float,
)
wide_profile = poisson_balloon_profile(wide, faces, boundary)
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)),
dtype=float,
)
faces = ((0, 1, 4), (1, 2, 4), (2, 3, 4), (3, 0, 4))
boundary = np.asarray((True, True, True, True, False))
nodes = np.asarray(((1, 1), (3, 1)), dtype=float)
with patch.object(
cutout_teddy,
"chordal_axis",
return_value=(nodes, [(0, 1)], np.asarray((1.0, 1.0))),
):
profile = cutout_teddy.teddy_balloon_heights(
points,
faces,
points[:4],
boundary,
0.45,
)
assert np.all(profile[:4] == 0.0)
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)),),
@@ -329,6 +400,23 @@ 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(
regions,
(100, 50),
(2.0, 6.0, -3.0, 1.0),
0.5,
((1.0, 0.0), (0.0, 1.0)),
)
center = (vertices[:, :2].min(axis=0) + vertices[:, :2].max(axis=0)) * 0.5
assert np.allclose(center, 0.0, atol=1e-6)
expected_uv = vertices[:, :2] / np.asarray((4.0, 4.0)) + 0.5
assert np.allclose(uv, expected_uv, atol=1e-6)
def test_cutout_domain_keeps_overlapping_regions_disconnected():
regions = (
(