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:
@@ -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 = (
|
||||
(
|
||||
|
||||
Reference in New Issue
Block a user