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.
This commit is contained in:
78
src/anyimage/server/geometry/edge_depth.py
Normal file
78
src/anyimage/server/geometry/edge_depth.py
Normal file
@@ -0,0 +1,78 @@
|
||||
"""Extend reliable Cutout depth along each visible component's silhouette."""
|
||||
|
||||
from dataclasses import replace
|
||||
|
||||
|
||||
def extend_edge_depth(frame, alpha, alpha_threshold=0.9):
|
||||
"""Return edge-corrected geometry while preserving reliable camera points."""
|
||||
import numpy as np
|
||||
from scipy import ndimage
|
||||
|
||||
from ..models.onnx_moge2 import depth_to_points
|
||||
|
||||
depth = frame.depth
|
||||
alpha = np.asarray(alpha, dtype=np.float32)
|
||||
if alpha.shape != depth.shape:
|
||||
raise ValueError("Edge Alpha must match the depth dimensions")
|
||||
if np.all(alpha == 1.0):
|
||||
return frame
|
||||
|
||||
visible = (alpha > 0.0) & (alpha >= alpha_threshold)
|
||||
candidates = (
|
||||
visible
|
||||
& (alpha >= 0.95)
|
||||
& (frame.validity > 0.0)
|
||||
& np.isfinite(depth)
|
||||
& (depth > 0.0)
|
||||
)
|
||||
if not candidates.any():
|
||||
return frame
|
||||
interior_distance = ndimage.distance_transform_edt(
|
||||
np.pad(candidates, 3, mode="edge")
|
||||
)[3:-3, 3:-3]
|
||||
core = candidates & (interior_distance > 2.0)
|
||||
del interior_distance
|
||||
labels, _ = ndimage.label(visible, structure=np.ones((3, 3), dtype=bool))
|
||||
corrected = depth.copy()
|
||||
background_distance = np.full(depth.shape, np.inf, dtype=np.float32)
|
||||
|
||||
for component_id, bounds in enumerate(ndimage.find_objects(labels), start=1):
|
||||
region = tuple(
|
||||
slice(max(0, bound.start - 7), min(size, bound.stop + 7))
|
||||
for bound, size in zip(bounds, depth.shape)
|
||||
)
|
||||
component = labels[region] == component_id
|
||||
donors = core[region] & component
|
||||
if not donors.any():
|
||||
donors = candidates[region] & component
|
||||
if not donors.any():
|
||||
continue
|
||||
|
||||
indices = ndimage.distance_transform_edt(
|
||||
~donors, return_distances=False, return_indices=True
|
||||
)
|
||||
extended = depth[region][tuple(indices)]
|
||||
smoothed = ndimage.gaussian_filter(extended, sigma=0.8, radius=3)
|
||||
distance = ndimage.distance_transform_edt(~component)
|
||||
background = (
|
||||
~visible[region]
|
||||
& (distance <= 4.0)
|
||||
& (distance < background_distance[region])
|
||||
)
|
||||
target = (component & ~donors) | background
|
||||
corrected[region][target] = smoothed[target]
|
||||
background_distance[region][background] = distance[background]
|
||||
|
||||
changed = corrected != depth
|
||||
if not changed.any():
|
||||
return frame
|
||||
points = frame.points
|
||||
if points is not None:
|
||||
height, width = depth.shape
|
||||
intrinsics = frame.intrinsics.copy()
|
||||
intrinsics[0] /= width
|
||||
intrinsics[1] /= height
|
||||
projected = depth_to_points(corrected, intrinsics)
|
||||
points = points.copy()
|
||||
points[changed] = projected[changed]
|
||||
return replace(frame, depth=corrected, points=points)
|
||||
@@ -8,6 +8,7 @@ from .depth_texture import (
|
||||
write_z_depth_texture,
|
||||
)
|
||||
from .normal_texture import write_camera_normal_texture
|
||||
from .edge_depth import extend_edge_depth
|
||||
|
||||
|
||||
def _limited_model_input(source_path, output_path, max_input_size):
|
||||
@@ -21,7 +22,7 @@ def _limited_model_input(source_path, output_path, max_input_size):
|
||||
return output_path
|
||||
|
||||
|
||||
def _alpha_mask(image_path, shape):
|
||||
def _image_alpha(image_path, shape):
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
@@ -31,13 +32,13 @@ def _alpha_mask(image_path, shape):
|
||||
(int(shape[1]), int(shape[0])),
|
||||
Image.Resampling.NEAREST,
|
||||
)
|
||||
return np.asarray(alpha, dtype=np.uint8) >= 26
|
||||
return np.asarray(alpha, dtype=np.float32) / 255.0
|
||||
|
||||
|
||||
def _write_normal(frame, color_path, output_path, normal_mode):
|
||||
from PIL import Image
|
||||
|
||||
valid = _alpha_mask(color_path, frame.depth.shape)
|
||||
valid = _image_alpha(color_path, frame.depth.shape) >= 0.1
|
||||
with Image.open(color_path) as source:
|
||||
output_size = source.size
|
||||
return write_camera_normal_texture(
|
||||
@@ -58,6 +59,8 @@ def generate_moge2_artifacts(
|
||||
*,
|
||||
depth_mode="NONE",
|
||||
normal_mode="NONE",
|
||||
correct_edge_depth=False,
|
||||
alpha_threshold=0.9,
|
||||
):
|
||||
"""Run MoGe-2 once and write only the requested file artifacts."""
|
||||
depth_mode = str(depth_mode).upper()
|
||||
@@ -85,21 +88,27 @@ def generate_moge2_artifacts(
|
||||
result = {}
|
||||
if depth_mode != "NONE":
|
||||
context.progress(0.45, "Generating depth texture")
|
||||
alpha = _image_alpha(input_path, frame.depth.shape)
|
||||
depth_frame = (
|
||||
extend_edge_depth(frame, alpha, alpha_threshold)
|
||||
if correct_edge_depth else frame
|
||||
)
|
||||
if depth_mode == "VECTOR":
|
||||
depth_path = write_vector_depth_texture(
|
||||
frame,
|
||||
depth_frame,
|
||||
context.directory / "vector-depth.exr",
|
||||
)
|
||||
result["vector_depth"] = depth_path
|
||||
else:
|
||||
depth_path = write_z_depth_texture(
|
||||
frame,
|
||||
depth_frame,
|
||||
context.directory / "z-depth.exr",
|
||||
)
|
||||
result["z_depth"] = depth_path
|
||||
result["depth_metadata"] = write_depth_metadata(
|
||||
frame,
|
||||
_alpha_mask(input_path, frame.depth.shape),
|
||||
depth_frame,
|
||||
(alpha > 0.0) & (alpha >= alpha_threshold)
|
||||
if correct_edge_depth else alpha >= 0.1,
|
||||
context.directory / "depth.json",
|
||||
)
|
||||
|
||||
|
||||
@@ -19,6 +19,8 @@ def run(context, parameters):
|
||||
input_path,
|
||||
depth_mode=depth_mode,
|
||||
normal_mode=normal_mode,
|
||||
correct_edge_depth=depth_mode.upper() != "NONE",
|
||||
alpha_threshold=float(parameters.get("alpha_threshold", 0.9)),
|
||||
)
|
||||
result.update({key: path.name for key, path in artifacts.items()})
|
||||
context.check_cancelled()
|
||||
|
||||
Reference in New Issue
Block a user