Add image selection refinement functionality with BEN2 integration
- Implemented viewport.py for handling 3D view projections, gestures, and overlay drawing. - Created refine_image_selection.py to manage the refinement of gesture masks and output image bounds. - Added selection module with refine_selection.py to process and crop images based on alpha channel visibility. - Introduced __init__.py for selection module to expose refine_selection function.
This commit is contained in:
@@ -10,21 +10,26 @@ from mathutils import Vector
|
||||
SOURCE_ROOT = Path(__file__).parents[1] / "src"
|
||||
sys.path.insert(0, str(SOURCE_ROOT))
|
||||
|
||||
from anyimage.common import image_data
|
||||
from anyimage.common import depth_data
|
||||
from anyimage.common import depth as depth_data
|
||||
from anyimage.common import image as image_data
|
||||
from anyimage.common import material as material_data
|
||||
from anyimage.common import node as node_asset
|
||||
from anyimage.common import object as object_data
|
||||
from anyimage.common.image_region import (
|
||||
RegionMask,
|
||||
RegionPath,
|
||||
rasterize_region_path,
|
||||
from anyimage.common.selection import (
|
||||
SelectionMask,
|
||||
SelectionPath,
|
||||
rasterize_selection_path,
|
||||
)
|
||||
from anyimage.operators.convert_to_plane import object as plane_object
|
||||
from anyimage.operators.cutout_tool import geometry as cutout_geometry
|
||||
from anyimage.operators.cutout_tool import main as cutout_main
|
||||
from anyimage.operators.cutout_tool import object as cutout_object
|
||||
from anyimage.operators.cutout_tool import shape as cutout_shape
|
||||
from anyimage.server.geometry.depth_texture import write_vector_depth_texture
|
||||
from anyimage.server.geometry.depth_texture import (
|
||||
write_vector_depth_texture,
|
||||
write_z_depth_texture,
|
||||
)
|
||||
from anyimage.server.models.geometry import GeometryFrame
|
||||
|
||||
|
||||
def _inputs(group):
|
||||
@@ -51,26 +56,26 @@ def _create_cutout_shape(
|
||||
bounds=None,
|
||||
depth_image=None,
|
||||
depth_metadata=None,
|
||||
region_values=None,
|
||||
selection_values=None,
|
||||
):
|
||||
if bounds is None:
|
||||
bounds = (0, 0, *tuple(int(value) for value in source.data.size))
|
||||
left, top, right, bottom = bounds
|
||||
if region_values is None:
|
||||
region_values = np.ones((bottom - top, right - left), dtype=np.float32)
|
||||
region_mask = RegionMask(region_values, bounds)
|
||||
if selection_values is None:
|
||||
selection_values = np.ones((bottom - top, right - left), dtype=np.float32)
|
||||
selection_mask = SelectionMask(selection_values, bounds)
|
||||
base_shape = cutout_geometry.build_base_shape(
|
||||
bpy.context,
|
||||
source,
|
||||
detail,
|
||||
region_mask,
|
||||
selection_mask,
|
||||
)
|
||||
return cutout_object.create_shape_object(
|
||||
bpy.context,
|
||||
source,
|
||||
shape,
|
||||
detail,
|
||||
region_mask,
|
||||
selection_mask,
|
||||
base_shape,
|
||||
depth_image=depth_image,
|
||||
depth_metadata=depth_metadata,
|
||||
@@ -82,7 +87,7 @@ def _depth_image(points, valid, name="Depth"):
|
||||
valid = np.asarray(valid, dtype=bool)
|
||||
height, width = valid.shape
|
||||
pixels = np.zeros((height, width, 4), dtype=np.float32)
|
||||
pixels[valid, :3] = points[valid]
|
||||
pixels[..., :3] = points
|
||||
pixels[..., 3] = valid
|
||||
image = bpy.data.images.new(
|
||||
name,
|
||||
@@ -112,9 +117,15 @@ def test_depth_result_image_loads_server_vector_exr_and_keeps_data_name(tmp_path
|
||||
source = bpy.data.objects.new("Source", None)
|
||||
source.data = source_image
|
||||
points = np.arange(12, dtype=np.float32).reshape((2, 2, 3))
|
||||
valid = np.asarray(((True, False), (True, True)))
|
||||
validity = np.asarray(((1.0, 0.0), (0.5, 1.0)), dtype=np.float32)
|
||||
frame = GeometryFrame(
|
||||
depth=points[..., 2].copy(),
|
||||
validity=validity,
|
||||
intrinsics=np.eye(3, dtype=np.float32),
|
||||
points=points,
|
||||
)
|
||||
path = tmp_path / "vector-depth.exr"
|
||||
write_vector_depth_texture(points, valid, path)
|
||||
write_vector_depth_texture(frame, path)
|
||||
metadata = _depth_metadata(np.eye(3), (2, 2), 1.0)
|
||||
|
||||
image = depth_data.load_depth_result_image(path, source, metadata)
|
||||
@@ -122,12 +133,38 @@ def test_depth_result_image_loads_server_vector_exr_and_keeps_data_name(tmp_path
|
||||
packed = np.empty(16, dtype=np.float32)
|
||||
image.pixels.foreach_get(packed)
|
||||
packed = np.flipud(packed.reshape((2, 2, 4)))
|
||||
assert np.array_equal(packed[valid, :3], points[valid])
|
||||
assert np.array_equal(packed[..., 3] > 0.5, valid)
|
||||
assert np.array_equal(packed[..., :3], points)
|
||||
assert np.array_equal(packed[..., 3], validity)
|
||||
assert image.alpha_mode == "CHANNEL_PACKED"
|
||||
assert image.name == "Source_depth.exr"
|
||||
|
||||
|
||||
def test_z_depth_texture_keeps_depth_outside_positive_validity(tmp_path):
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
source_image = bpy.data.images.new("Source.png", width=2, height=1)
|
||||
source = bpy.data.objects.new("Source", None)
|
||||
source.data = source_image
|
||||
frame = GeometryFrame(
|
||||
depth=np.asarray(((2.0, 7.0),), dtype=np.float32),
|
||||
validity=np.asarray(((1.0, 0.0),), dtype=np.float32),
|
||||
intrinsics=np.eye(3, dtype=np.float32),
|
||||
)
|
||||
path = tmp_path / "z-depth.exr"
|
||||
write_z_depth_texture(frame, path)
|
||||
|
||||
image = depth_data.load_depth_result_image(
|
||||
path,
|
||||
source,
|
||||
_depth_metadata(np.eye(3), (2, 1), 2.0),
|
||||
)
|
||||
|
||||
packed = np.empty(8, dtype=np.float32)
|
||||
image.pixels.foreach_get(packed)
|
||||
packed = packed.reshape((1, 2, 4))
|
||||
assert np.array_equal(packed[..., :3], np.repeat(frame.depth[..., None], 3, axis=2))
|
||||
assert np.array_equal(packed[..., 3], frame.validity)
|
||||
|
||||
|
||||
def test_cutout_modifier_wraps_four_menu_controlled_shape_groups():
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
|
||||
@@ -370,8 +407,8 @@ def test_cutout_modifier_wraps_four_menu_controlled_shape_groups():
|
||||
def test_image_layer_switches_between_tangent_and_object_space_normal():
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
|
||||
assert object_data.IMAGE_MATERIAL_NODE_GROUP_NAME == "O Image Layer"
|
||||
group = object_data.material_node_group()
|
||||
assert material_data.IMAGE_MATERIAL_NODE_GROUP_NAME == "O Image Layer"
|
||||
group = material_data.material_node_group()
|
||||
normal = next(
|
||||
item
|
||||
for item in group.interface.items_tree
|
||||
@@ -416,7 +453,7 @@ def test_image_layer_switches_between_tangent_and_object_space_normal():
|
||||
|
||||
color = bpy.data.images.new("Color", width=2, height=2)
|
||||
normal_image = bpy.data.images.new("Normal", width=2, height=2)
|
||||
object_material = object_data.create_image_material(
|
||||
object_material = material_data.create_image_material(
|
||||
SimpleNamespace(data=SimpleNamespace(name="Object Normal")),
|
||||
color,
|
||||
1.0,
|
||||
@@ -426,7 +463,7 @@ def test_image_layer_switches_between_tangent_and_object_space_normal():
|
||||
layer = _group_node(group, object_material.node_tree)
|
||||
assert layer.inputs["Object Space"].default_value is True
|
||||
|
||||
tangent_material = object_data.create_image_material(
|
||||
tangent_material = material_data.create_image_material(
|
||||
SimpleNamespace(data=SimpleNamespace(name="Tangent Normal")),
|
||||
color,
|
||||
1.0,
|
||||
@@ -434,7 +471,7 @@ def test_image_layer_switches_between_tangent_and_object_space_normal():
|
||||
)
|
||||
layer = _group_node(group, tangent_material.node_tree)
|
||||
assert layer.inputs["Object Space"].default_value is False
|
||||
reused = object_data.material_node_group()
|
||||
reused = material_data.material_node_group()
|
||||
assert {
|
||||
node.space for node in reused.nodes if node.bl_idname == "ShaderNodeNormalMap"
|
||||
} == {"TANGENT", "OBJECT"}
|
||||
@@ -503,7 +540,7 @@ def test_balloon_build_starts_from_the_unified_single_sheet():
|
||||
)
|
||||
assert normal_scale_attribute.attribute_type == "GEOMETRY"
|
||||
assert normal_scale_attribute.attribute_name == cutout_shape.BALLOON_ATTRIBUTE_NAME
|
||||
image_layer = _group_node(object_data.material_node_group(), material_tree)
|
||||
image_layer = _group_node(material_data.material_node_group(), material_tree)
|
||||
normal_scale_link = next(
|
||||
link
|
||||
for link in material_tree.links
|
||||
@@ -572,7 +609,7 @@ def test_cutout_material_uses_flat_tangent_normal():
|
||||
color = bpy.data.images.new("Color", width=2, height=2)
|
||||
normal = bpy.data.images.new("Normal", width=1, height=1)
|
||||
|
||||
result = object_data.create_image_material(
|
||||
result = material_data.create_image_material(
|
||||
source,
|
||||
color,
|
||||
0.0,
|
||||
@@ -580,7 +617,7 @@ def test_cutout_material_uses_flat_tangent_normal():
|
||||
scene=bpy.context.scene,
|
||||
)
|
||||
|
||||
layer = _group_node(object_data.material_node_group(), result.node_tree)
|
||||
layer = _group_node(material_data.material_node_group(), result.node_tree)
|
||||
assert layer.inputs["Object Space"].default_value is False
|
||||
|
||||
|
||||
@@ -624,7 +661,7 @@ def test_depth_surface_protected_smooth_evaluates():
|
||||
== cutout_shape.CUTOUT_SHAPE_INDICES["DEPTH_SURFACE"]
|
||||
)
|
||||
expected_cleanup = cutout_geometry.cutout_cleanup_value(
|
||||
cutout_geometry.cutout_point_spacing(source, 8),
|
||||
cutout_geometry.cutout_sampling_spacing(source, 8),
|
||||
cutout_geometry.world_plane_metric(source.matrix_world)[0],
|
||||
)
|
||||
assert np.isclose(
|
||||
@@ -703,6 +740,44 @@ def test_depth_surface_protected_smooth_evaluates():
|
||||
assert np.isfinite(internal_positions).all()
|
||||
|
||||
|
||||
def test_depth_surface_keeps_continuous_points_across_zero_validity_edge():
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
image = bpy.data.images.new("Surface", width=64, height=64)
|
||||
source = bpy.data.objects.new("Surface", None)
|
||||
source.empty_display_type = "IMAGE"
|
||||
source.data = image
|
||||
source.empty_display_size = 2.0
|
||||
bpy.context.collection.objects.link(source)
|
||||
size = 8
|
||||
depth = np.ones((size, size), dtype=np.float32)
|
||||
x, y = np.meshgrid(np.arange(size), np.arange(size))
|
||||
intrinsics = np.asarray(
|
||||
((8.0, 0.0, 3.5), (0.0, 8.0, 3.5), (0.0, 0.0, 1.0)),
|
||||
dtype=np.float32,
|
||||
)
|
||||
points = np.empty((size, size, 3), dtype=np.float32)
|
||||
points[..., 0] = (x - intrinsics[0, 2]) / intrinsics[0, 0] * depth
|
||||
points[..., 1] = (y - intrinsics[1, 2]) / intrinsics[1, 1] * depth
|
||||
points[..., 2] = depth
|
||||
validity = np.ones((size, size), dtype=bool)
|
||||
validity[[0, -1], :] = False
|
||||
validity[:, [0, -1]] = False
|
||||
|
||||
result = _create_cutout_shape(
|
||||
source,
|
||||
"DEPTH_SURFACE",
|
||||
8,
|
||||
depth_image=_depth_image(points, validity),
|
||||
depth_metadata=_depth_metadata(intrinsics, (size, size), 1.0),
|
||||
)
|
||||
positions = _evaluated_positions(result)
|
||||
|
||||
assert np.isfinite(positions).all()
|
||||
assert np.ptp(positions[:, 0]) < 1e-4
|
||||
assert np.ptp(positions[:, 1]) > 1.5
|
||||
assert np.ptp(positions[:, 2]) > 1.5
|
||||
|
||||
|
||||
def _evaluated_positions(obj):
|
||||
evaluated = obj.evaluated_get(bpy.context.evaluated_depsgraph_get())
|
||||
result = evaluated.to_mesh()
|
||||
@@ -951,8 +1026,8 @@ def test_depth_balloon_uses_metric_plane_inputs_without_controller():
|
||||
depth_image = _depth_image(z_depth, valid)
|
||||
depth_metadata = _depth_metadata(intrinsics, (41, 61), 2.0)
|
||||
|
||||
region_values = np.ones((60, 40), dtype=np.float32)
|
||||
region_values[:, :2] = 0.0
|
||||
selection_values = np.ones((60, 40), dtype=np.float32)
|
||||
selection_values[:, :2] = 0.0
|
||||
result = _create_cutout_shape(
|
||||
source,
|
||||
"DEPTH_BALLOON",
|
||||
@@ -960,7 +1035,7 @@ def test_depth_balloon_uses_metric_plane_inputs_without_controller():
|
||||
depth_image=depth_image,
|
||||
depth_metadata=depth_metadata,
|
||||
bounds=crop_bounds,
|
||||
region_values=region_values,
|
||||
selection_values=selection_values,
|
||||
)
|
||||
|
||||
assert all(abs(vertex.co.x) < 1e-8 for vertex in result.data.vertices)
|
||||
@@ -1135,16 +1210,16 @@ def test_local_cutout_selection_matches_uint8_tenth_alpha_boundary():
|
||||
top_down = np.ones((4, 4, 4), dtype=np.float32)
|
||||
top_down[1, 1, 3] = 25.0 / 255.0
|
||||
top_down[1, 2, 3] = 26.0 / 255.0
|
||||
region_mask = rasterize_region_path(
|
||||
selection_mask = rasterize_selection_path(
|
||||
(4, 4),
|
||||
RegionPath(
|
||||
SelectionPath(
|
||||
points=((0, 0), (4, 0), (4, 4), (0, 4)),
|
||||
fill_rule="NONZERO",
|
||||
),
|
||||
source_alpha=top_down[:, :, 3],
|
||||
alpha_threshold=0.1,
|
||||
)
|
||||
selection = region_mask.full_values((4, 4))
|
||||
selection = selection_mask.full_values((4, 4))
|
||||
|
||||
assert selection[1, 1] == 0.0
|
||||
assert selection[1, 2] > 0.1
|
||||
@@ -1162,7 +1237,7 @@ def test_local_cutout_builds_from_blender_image_pixels_without_a_job():
|
||||
bpy.context.collection.objects.link(source)
|
||||
source.select_set(True)
|
||||
bpy.context.view_layer.objects.active = source
|
||||
region_path = RegionPath(
|
||||
selection_path = SelectionPath(
|
||||
points=((-8, 8), (72, 8), (72, 40), (-8, 40)),
|
||||
fill_rule="NONZERO",
|
||||
)
|
||||
@@ -1174,9 +1249,9 @@ def test_local_cutout_builds_from_blender_image_pixels_without_a_job():
|
||||
wraps=image_data.image_rgba,
|
||||
) as read_rgba:
|
||||
rgba = image_data.image_rgba(image)
|
||||
region_mask = rasterize_region_path(
|
||||
selection_mask = rasterize_selection_path(
|
||||
tuple(image.size),
|
||||
region_path,
|
||||
selection_path,
|
||||
source_alpha=rgba[:, :, 3],
|
||||
alpha_threshold=cutout_geometry.CUTOUT_ALPHA_THRESHOLD,
|
||||
antialias=True,
|
||||
@@ -1186,7 +1261,7 @@ def test_local_cutout_builds_from_blender_image_pixels_without_a_job():
|
||||
source,
|
||||
"SURFACE",
|
||||
16,
|
||||
region_mask,
|
||||
selection_mask,
|
||||
source_rgba=rgba,
|
||||
)
|
||||
|
||||
@@ -1203,7 +1278,7 @@ def test_local_cutout_builds_from_blender_image_pixels_without_a_job():
|
||||
== cutout_shape.CUTOUT_SHAPE_INDICES["SURFACE"]
|
||||
)
|
||||
expected_cleanup = cutout_geometry.cutout_cleanup_value(
|
||||
cutout_geometry.cutout_point_spacing(source, 16),
|
||||
cutout_geometry.cutout_sampling_spacing(source, 16),
|
||||
cutout_geometry.world_plane_metric(source.matrix_world)[0],
|
||||
)
|
||||
assert np.isclose(
|
||||
@@ -1245,7 +1320,7 @@ def test_local_cutout_builds_from_blender_image_pixels_without_a_job():
|
||||
color_texture.image.pixels.foreach_get(copied)
|
||||
copied = copied.reshape((34, 64, 4))
|
||||
assert np.allclose(copied[:, :, :3], 1.0)
|
||||
left, top, right, bottom = region_mask.bounds
|
||||
left, top, right, bottom = selection_mask.bounds
|
||||
assert np.allclose(
|
||||
copied[:, :, 3], np.flipud(rgba[top:bottom, left:right, 3])
|
||||
)
|
||||
@@ -1257,7 +1332,7 @@ def test_balloon_normal_scale_uses_existing_image_layer_input():
|
||||
{"BALLOON", "DEPTH_BALLOON"}
|
||||
)
|
||||
group = bpy.data.node_groups.new(
|
||||
object_data.IMAGE_MATERIAL_NODE_GROUP_NAME,
|
||||
material_data.IMAGE_MATERIAL_NODE_GROUP_NAME,
|
||||
"ShaderNodeTree",
|
||||
)
|
||||
group.interface.new_socket(
|
||||
|
||||
Reference in New Issue
Block a user