- Removed `Smooth Weight` input from `O Image Depth Plane`, `O Image Depth Cutout`, and `O Image Depth Panorama` to streamline the interface and maintain consistent boundary smoothing behavior. - Adjusted the `Reference Depth` input to appear last in the options panel for better organization, reflecting its role as a calibration value. - Consolidated the `expand_smooth()` function to only accept necessary parameters, eliminating unused branches related to `Smooth Weight` and `pin_sharp`. - Updated related tests and node assets to ensure functionality remains intact and consistent with the new structure. - Introduced new specifications to document the changes and ensure compliance with the updated requirements.
183 lines
9.0 KiB
Python
183 lines
9.0 KiB
Python
"""Verify boundary depth filtering before camera and radial projection."""
|
|
|
|
import bpy
|
|
import numpy as np
|
|
import pytest
|
|
|
|
from tests.support.depth_surface import evaluated
|
|
from tests.tools.nodes.test_boundary_smoothing import boundary_neighbors, edge_band
|
|
from tools.nodes.common.boundary_smoothing import smooth_boundary_camera
|
|
|
|
|
|
def test_camera_blur_preserves_rays_interior_protection_and_disconnected_surfaces():
|
|
bpy.ops.wm.read_factory_settings(use_empty=True)
|
|
points, faces, cameras, protected = [], [], [], []
|
|
for offset, depth in ((0, 2), (81, 50)):
|
|
for y in range(9):
|
|
for x in range(9):
|
|
value = 20 if offset == 0 and (x, y) == (4, 0) else depth
|
|
if offset == 0 and (x, y) == (8, 8):
|
|
value = 0
|
|
points.append((x, y, 0))
|
|
cameras.append((x * value / 10, y * value / 10, value))
|
|
protected.append(x == 0)
|
|
faces.extend((offset + y * 9 + x, offset + y * 9 + x + 1,
|
|
offset + (y + 1) * 9 + x + 1, offset + (y + 1) * 9 + x)
|
|
for y in range(8) for x in range(8))
|
|
mesh = bpy.data.meshes.new("Disconnected camera samples")
|
|
mesh.from_pydata(points, [], faces)
|
|
camera = np.asarray(cameras, np.float32)
|
|
mesh.attributes.new("test_camera", "FLOAT_VECTOR", "POINT").data.foreach_set("vector", camera.ravel())
|
|
mesh.attributes.new("test_protected", "BOOLEAN", "POINT").data.foreach_set("value", protected)
|
|
obj = bpy.data.objects.new("Depth samples", mesh)
|
|
bpy.context.collection.objects.link(obj)
|
|
group = bpy.data.node_groups.new("Boundary depth test", "GeometryNodeTree")
|
|
for direction in ("INPUT", "OUTPUT"):
|
|
group.interface.new_socket(name="Geometry", in_out=direction, socket_type="NodeSocketGeometry")
|
|
control = group.interface.new_socket(name="Boundary Smooth", in_out="INPUT", socket_type="NodeSocketInt")
|
|
source = group.nodes.new("NodeGroupInput")
|
|
attributes = []
|
|
for name, kind in (("test_camera", "FLOAT_VECTOR"), ("test_protected", "BOOLEAN")):
|
|
node = group.nodes.new("GeometryNodeInputNamedAttribute")
|
|
node.data_type = kind
|
|
node.inputs["Name"].default_value = name
|
|
attributes.append(node.outputs["Attribute"])
|
|
position = group.nodes.new("GeometryNodeSetPosition")
|
|
group.links.new(source.outputs["Geometry"], position.inputs["Geometry"])
|
|
group.links.new(smooth_boundary_camera(group, *attributes), position.inputs["Position"])
|
|
output = group.nodes.new("NodeGroupOutput")
|
|
group.links.new(position.outputs[0], output.inputs["Geometry"])
|
|
modifier = obj.modifiers.new("Depth", "NODES")
|
|
modifier.node_group = group
|
|
modifier[control.identifier] = 0
|
|
np.testing.assert_array_equal(evaluated(obj)[0], camera)
|
|
modifier[control.identifier] = 12
|
|
obj.update_tag(refresh={"DATA"})
|
|
bpy.context.view_layer.update()
|
|
result, result_faces = evaluated(obj)
|
|
assert result_faces == faces
|
|
assert 2 < result[4, 2] < 10
|
|
assert np.isfinite(result).all()
|
|
np.testing.assert_array_equal(result[protected], camera[protected])
|
|
np.testing.assert_array_equal(result[81:], camera[81:])
|
|
np.testing.assert_array_equal(result[80], camera[80])
|
|
band = edge_band(faces, set(boundary_neighbors(faces)) - set(np.flatnonzero(protected)))
|
|
interior = [i for i in range(len(camera)) if i not in band]
|
|
assert interior
|
|
np.testing.assert_array_equal(result[interior], camera[interior])
|
|
valid = camera[:, 2] > 0
|
|
np.testing.assert_allclose(result[valid, :2] / result[valid, 2:3],
|
|
camera[valid, :2] / camera[valid, 2:3], atol=1e-6)
|
|
|
|
|
|
|
|
def test_cutout_fine_outline_smooths_only_within_boundary_band():
|
|
from tests.support.depth_surface import surface
|
|
from anyimage.operators.cutout_tool.mesh import build_mesh
|
|
|
|
obj, set_value = surface(step=0, resolution=8)
|
|
yy, xx = np.mgrid[:32, :64]
|
|
alpha = ((xx - 32)**2 / 27**2 + (yy - 16)**2 / 13**2 < 1).astype(float)
|
|
points, faces, _, _, _ = build_mesh(alpha, 3, .9)
|
|
obj.data.clear_geometry()
|
|
obj.data.from_pydata([(x / 32, y / 32, 0) for x, y in points], [], faces.tolist())
|
|
uv = obj.data.uv_layers.get("UVMap") or obj.data.uv_layers.new(name="UVMap")
|
|
for loop in obj.data.loops:
|
|
uv.data[loop.index].uv = points[loop.vertex_index] / (64, 32)
|
|
image = bpy.data.images["Camera"]
|
|
pixels = np.array(image.pixels[:], np.float32).reshape(32, 64, 4)
|
|
pixels[..., 2] = 2 + 12 * ((xx > 25) & (xx < 39) & (yy > 25))
|
|
image.pixels.foreach_set(pixels.ravel())
|
|
image.update()
|
|
set_value("Boundary Smooth", 0)
|
|
before, original_faces = evaluated(obj)
|
|
set_value("Boundary Smooth", 12)
|
|
after, result_faces = evaluated(obj)
|
|
assert result_faces == original_faces
|
|
band = edge_band(original_faces, boundary_neighbors(original_faces))
|
|
interior = [i for i in range(len(before)) if i not in band]
|
|
assert interior
|
|
np.testing.assert_array_equal(after[interior], before[interior])
|
|
assert np.max(np.linalg.norm(after - before, axis=1)) > 1e-4
|
|
set_value("Boundary Smooth", 0)
|
|
np.testing.assert_array_equal(evaluated(obj)[0], before)
|
|
|
|
|
|
def test_plane_validity_hole_blurs_depth_and_protects_rectangle():
|
|
from tests.support.planes import create_surface, evaluated as plane_mesh
|
|
|
|
obj, set_value, _, _, image = create_surface("DEPTH")
|
|
pixels = np.array(image.pixels[:], np.float32).reshape(8, 16, 4)
|
|
pixels[..., 2] = 2
|
|
pixels[2:6, 6:10, 3] = 0
|
|
pixels[1:3, 7:9, 2] = 12
|
|
image.pixels.foreach_set(pixels.ravel())
|
|
image.update()
|
|
for name, value in (("Subdivide", 5), ("Depth Scale", 1)):
|
|
set_value(name, value)
|
|
before, faces, uv, *_ = plane_mesh(obj)
|
|
set_value("Boundary Smooth", 12)
|
|
after, result_faces, result_uv, *_ = plane_mesh(obj)
|
|
assert faces == result_faces
|
|
np.testing.assert_array_equal(result_uv, uv)
|
|
assert after[:, 1].max() < before[:, 1].max() - .1
|
|
protected = np.any(np.isclose(uv, 0) | np.isclose(uv, 1), axis=1)
|
|
corners = np.concatenate(faces)
|
|
outer = np.unique(corners[protected])
|
|
np.testing.assert_array_equal(after[outer], before[outer])
|
|
# Position smoothing moves the cut band off its projection ray by design.
|
|
band = edge_band(faces, boundary_neighbors(faces))
|
|
kept = [i for i in range(len(before)) if i not in band]
|
|
np.testing.assert_allclose(after[kept][:, [0, 2]] / (1.2 + after[kept][:, 1:2]),
|
|
before[kept][:, [0, 2]] / (1.2 + before[kept][:, 1:2]), atol=1e-6)
|
|
set_value("Depth Mask", False)
|
|
intact = plane_mesh(obj)[0]
|
|
set_value("Boundary Smooth", 0)
|
|
np.testing.assert_array_equal(plane_mesh(obj)[0], intact)
|
|
|
|
|
|
@pytest.mark.parametrize("region", ["seam", "pole"])
|
|
def test_panorama_blurs_opening_radially_and_keeps_invalid_distance(region):
|
|
from tests.tools.nodes.test_image_depth_panorama import panorama, evaluated as panorama_mesh
|
|
|
|
yy, xx = np.mgrid[:64, :128]
|
|
distance = np.minimum(xx, 128 - xx)
|
|
hole = (distance**2 + (yy - 32)**2 < 9**2) if region == "seam" else (yy > 54)
|
|
spike = ((distance < 4) & (yy > 40) & (yy < 44)) if region == "seam" else ((yy > 50) & (xx < 8))
|
|
depth = np.where(spike, 10., 3.)
|
|
obj, set_value = panorama(depth, (~hole).astype(float), subdivide=5)
|
|
before, faces, uv = panorama_mesh(obj)
|
|
set_value("Boundary Smooth", 12)
|
|
after, result_faces, result_uv = panorama_mesh(obj)
|
|
assert result_faces == faces
|
|
np.testing.assert_array_equal(result_uv, uv)
|
|
radii = np.linalg.norm(before, axis=1)
|
|
smoothed = np.linalg.norm(after, axis=1)
|
|
rim = list(boundary_neighbors(faces))
|
|
assert smoothed[rim].max() < radii[rim].max() - .1
|
|
band = edge_band(faces, boundary_neighbors(faces))
|
|
interior = [i for i in range(len(before)) if i not in band]
|
|
assert interior
|
|
# Position smoothing moves the opening rim off its radial direction by design.
|
|
np.testing.assert_allclose(after[interior] / smoothed[interior, None],
|
|
before[interior] / radii[interior, None], atol=1e-6)
|
|
np.testing.assert_array_equal(after[interior], before[interior])
|
|
set_value("Depth Mask", False)
|
|
near = panorama_mesh(obj)[0]
|
|
set_value("Invalid Distance", 250)
|
|
far, far_faces, _ = panorama_mesh(obj)
|
|
valid = np.linalg.norm(near, axis=1) < 50
|
|
assert valid.any() and (~valid).any()
|
|
np.testing.assert_array_equal(far[valid], near[valid])
|
|
# The smoothed rim blends into its neighbours, so only the interior keeps the exact distance.
|
|
far_band = edge_band(far_faces, boundary_neighbors(far_faces))
|
|
kept = [i for i in range(len(far)) if i not in far_band and not valid[i]]
|
|
assert kept
|
|
np.testing.assert_allclose(np.linalg.norm(far[kept], axis=1), 250, atol=5e-5)
|
|
set_value("Depth Scale", 0)
|
|
sphere, sphere_faces, _ = panorama_mesh(obj)
|
|
sphere_band = edge_band(sphere_faces, boundary_neighbors(sphere_faces))
|
|
interior = [i for i in range(len(sphere)) if i not in sphere_band]
|
|
assert interior
|
|
np.testing.assert_allclose(np.linalg.norm(sphere[interior], axis=1), 1, atol=1e-6)
|