Files
AnyImage/tests/test_edge_depth.py
nan 7e55af6722 Add comprehensive tests for edge depth extension, empty image edits, and material view adaptation
- Introduced `test_edge_depth.py` to validate the functionality of edge depth extension, ensuring core alpha preservation, proper depth handling, and edge padding behavior.
- Created `test_empty_image_edits.py` to verify the behavior of image edits in Blender, focusing on shared edits, preservation of image properties, and handling of failed operations.
- Developed `test_material_view_adaptation.py` to test material adaptation preferences, ensuring color space and alpha settings are correctly applied and preserved across various scenarios.
2026-09-06 21:43:47 +08:00

222 lines
7.3 KiB
Python

import sys
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import patch
import numpy as np
import pytest
from PIL import Image
sys.path.insert(0, str(Path(__file__).parents[1] / "src" / "anyimage"))
from server.geometry import prediction_artifacts as artifacts
from server.geometry.edge_depth import extend_edge_depth
from server.models.geometry import GeometryFrame
from server.models.onnx_moge2 import depth_to_points
def make_frame(depth):
depth = np.asarray(depth, dtype=np.float32)
height, width = depth.shape
normalized = np.array([[1.2, 0, 0.4], [0, 1.4, 0.6], [0, 0, 1]], np.float32)
intrinsics = normalized.copy()
intrinsics[0] *= width
intrinsics[1] *= height
return GeometryFrame(
depth=depth,
validity=np.ones_like(depth),
intrinsics=intrinsics,
points=depth_to_points(depth, normalized),
normal=np.zeros((*depth.shape, 3), np.float32),
)
def silhouette():
alpha = np.zeros((25, 35), np.float32)
alpha[4:21, 4:22] = 1
alpha[4:21, 22:25] = 0.2
depth = np.full(alpha.shape, 20, np.float32)
depth[alpha == 1] = 2
return make_frame(depth), alpha
def test_hair_extension_preserves_core_alpha_and_projection():
frame, alpha = silhouette()
original_alpha = alpha.copy()
result = extend_edge_depth(frame, alpha)
assert np.all(result.depth[alpha >= 0.1] == 2)
np.testing.assert_array_equal(result.points[9:16, 9:16], frame.points[9:16, 9:16])
np.testing.assert_array_equal(alpha, original_alpha)
np.testing.assert_array_equal(frame.depth[alpha == 0.2], 20)
np.testing.assert_allclose(
result.points[..., :2] / result.points[..., 2:],
frame.points[..., :2] / frame.points[..., 2:],
atol=1e-6,
)
assert result.validity is frame.validity
assert result.normal is frame.normal
assert np.isfinite(result.points).all()
def test_sampling_padding_is_bounded():
frame, alpha = silhouette()
result = extend_edge_depth(frame, alpha, alpha_threshold=0.1)
assert np.all(result.depth[12, 24:29] == 2)
assert result.depth[12, 29] == 20
assert (result.depth[12, 24] + result.depth[12, 25]) / 2 == 2
def test_edge_padding_tracks_selected_threshold():
frame, alpha = silhouette()
loose = extend_edge_depth(frame, alpha, alpha_threshold=0.1)
strict = extend_edge_depth(frame, alpha)
assert loose.depth[12, 28] == 2
assert strict.depth[12, 28] == 20
def test_strict_threshold_excludes_lower_alpha_donors():
alpha = np.zeros((10, 10), np.float32)
alpha[3:7, 3:7] = 0.96
alpha[5, 5] = 1
depth = np.full(alpha.shape, 10, np.float32)
depth[5, 5] = 2
result = extend_edge_depth(make_frame(depth), alpha, alpha_threshold=1)
assert result.depth[5, 6] == 2
def test_image_rectangle_does_not_erode_subject():
alpha = np.ones((15, 20), np.float32)
alpha[:, 15:] = 0
depth = np.full(alpha.shape, 2, np.float32)
depth[0, 0] = 3
frame = make_frame(depth)
result = extend_edge_depth(frame, alpha)
assert result.depth[0, 0] == 3
np.testing.assert_array_equal(result.points[0, 0], frame.points[0, 0])
def test_small_components_fall_back_without_borrowing_other_depths():
alpha = np.zeros((15, 30), np.float32)
alpha[5, 5:7] = [0.95, 0.1]
alpha[5, 10:12] = [1, 0.2]
alpha[5, 14] = 0.94
depth = np.full(alpha.shape, 30, np.float32)
depth[5, 5] = 2
depth[5, 10] = 8
result = extend_edge_depth(make_frame(depth), alpha)
assert result.depth[5, 6] == 2
assert result.depth[5, 11] == 8
assert result.depth[5, 14] == 30
assert result.depth[5, 7] == 2
assert result.depth[5, 9] == 8
def test_diagonal_pixels_share_a_component():
alpha = np.zeros((10, 10), np.float32)
alpha[4, 4] = 1
alpha[5, 5] = 0.2
depth = np.full(alpha.shape, 10, np.float32)
depth[4, 4] = 2
result = extend_edge_depth(make_frame(depth), alpha)
assert result.depth[5, 5] == 2
def test_invalid_donors_are_excluded():
frame, alpha = silhouette()
frame.depth[10, 12] = 100
frame.validity[10, 12] = 0
result = extend_edge_depth(frame, alpha)
assert result.depth[10, 12] == 2
assert result.validity[10, 12] == 0
def test_local_smoothing_reduces_donor_step_and_preserves_core():
frame, alpha = silhouette()
frame.depth[12:, alpha.shape[1] // 4:22] = 4
result = extend_edge_depth(frame, alpha)
assert 2 < result.depth[11, 23] < 4
assert 2 < result.depth[12, 23] < 4
np.testing.assert_array_equal(result.depth[8:17, 8:17], frame.depth[8:17, 8:17])
@pytest.mark.parametrize("alpha_value", [0, 0.3, 1])
def test_images_without_repair_return_original_frame(alpha_value):
frame = make_frame(np.ones((5, 8)))
assert extend_edge_depth(frame, np.full((5, 8), alpha_value)) is frame
def test_alpha_resize_retains_threshold_values(tmp_path):
rgba = np.zeros((1, 4, 4), np.uint8)
rgba[0, :, 3] = [25, 26, 242, 243]
path = tmp_path / "alpha.png"
Image.fromarray(rgba).save(path)
alpha = artifacts._image_alpha(path, (2, 8))
np.testing.assert_array_equal(
alpha >= 0.1, [[False] * 2 + [True] * 6] * 2
)
np.testing.assert_array_equal(
alpha >= 0.95, [[False] * 6 + [True] * 2] * 2
)
@pytest.mark.parametrize(
"depth_mode,normal_mode,correct",
[
("VECTOR", "OBJECT", True),
("VECTOR", "TANGENT", True),
("Z", "TANGENT", True),
("Z", "TANGENT", False),
("NONE", "OBJECT", True),
],
)
def test_artifacts_use_corrected_depth_and_original_normals(
tmp_path, depth_mode, normal_mode, correct
):
frame, alpha = silhouette()
if depth_mode == "Z":
frame.points = None
rgba = np.full((*alpha.shape, 4), 255, np.uint8)
rgba[..., 3] = np.round(alpha * 255).astype(np.uint8)
path = tmp_path / "source.png"
Image.fromarray(rgba).save(path)
context = SimpleNamespace(
directory=tmp_path,
resource=lambda _: None,
progress=lambda *_: None,
check_cancelled=lambda: None,
)
with (
patch.object(artifacts.moge2, "infer", return_value=[frame]),
patch.object(
artifacts, "write_vector_depth_texture",
return_value=tmp_path / "vector.exr",
) as vector,
patch.object(
artifacts, "write_z_depth_texture", return_value=tmp_path / "z.exr"
) as z,
patch.object(
artifacts, "write_depth_metadata", return_value=tmp_path / "depth.json"
) as metadata,
patch.object(
artifacts, "_write_normal", return_value=tmp_path / "normal.png"
) as normal,
patch.object(artifacts, "extend_edge_depth", wraps=extend_edge_depth) as repair,
):
artifacts.generate_moge2_artifacts(
context, {}, path, depth_mode=depth_mode, normal_mode=normal_mode,
correct_edge_depth=correct,
)
assert normal.call_args.args[0] is frame
assert repair.call_count == int(correct and depth_mode != "NONE")
if depth_mode != "NONE":
written_frame = (vector if depth_mode == "VECTOR" else z).call_args.args[0]
assert metadata.call_args.args[0] is written_frame
if correct:
assert written_frame.depth[12, 24] == 2
else:
assert written_frame is frame
else:
metadata.assert_not_called()
vector.assert_not_called()
z.assert_not_called()