feat(cutout_tool): Implement cutout region to shape functionality

- Added interaction helpers for cutout region validation and shape menu interaction.
- Created main operator for converting image regions into cutout shapes, including AI artifact generation.
- Introduced object creation logic for cutout shapes, handling depth and normal textures.
- Defined cutout shape identifiers and node groups for various shape types.
- Implemented depth texture writing and prediction artifact generation for MoGe-2.
- Developed server jobs for generating cutout artifacts, depth planes, and refining image regions.
- Added region refinement logic to enhance gesture masks using BEN2.
This commit is contained in:
2026-08-31 11:36:27 +08:00
parent 08309f719d
commit 87d7721128
80 changed files with 4823 additions and 4297 deletions

View File

@@ -1,4 +1,3 @@
import json
import sys
from pathlib import Path
from types import MethodType, SimpleNamespace
@@ -9,8 +8,9 @@ SOURCE_ROOT = Path(__file__).parents[1] / "src"
sys.path.insert(0, str(SOURCE_ROOT))
from anyimage.operators.common.image_interaction import ImageRegionGesture
from anyimage.operators.cutout_tool import select_region as cutout
from anyimage.operators.common.image_region import RegionPath
from anyimage.operators.cutout_tool import interaction
from anyimage.operators.cutout_tool import main as cutout
class Pie:
@@ -32,10 +32,10 @@ class WindowManager:
draw(menu, None)
def cutout_context(window_manager, point_spacing=32, normal_mode="NONE"):
def cutout_context(window_manager, point_spacing=32, generate_normal=False):
settings = SimpleNamespace(
cutout_point_spacing=point_spacing,
cutout_normal_mode=normal_mode,
cutout_generate_normal=generate_normal,
)
return SimpleNamespace(
window_manager=window_manager,
@@ -52,7 +52,7 @@ def test_cutout_pie_uses_four_semantic_shapes_in_cardinal_order():
context = cutout_context(window_manager)
source = cutout_source()
cutout._open_shape_pie(
interaction.open_shape_pie(
context,
object(),
source,
@@ -75,19 +75,17 @@ def test_cutout_pie_uses_four_semantic_shapes_in_cardinal_order():
call[0] == "anyimage.cutout_region_to_shape"
for call in window_manager.pie.calls
)
assert not any(call[2].refine_selection for call in window_manager.pie.calls)
assert all(call[2].normal_mode == "NONE" for call in window_manager.pie.calls)
assert all(call[2].mesh_detail == 64 for call in window_manager.pie.calls)
assert not any(call[2].refine_region for call in window_manager.pie.calls)
assert not any(call[2].generate_normal for call in window_manager.pie.calls)
assert all(call[2].mesh_resolution == 64 for call in window_manager.pie.calls)
def test_cutout_pie_passes_image_lasso_options_to_every_shape():
window_manager = WindowManager()
context = cutout_context(
window_manager, point_spacing=16, normal_mode="OBJECT"
)
context = cutout_context(window_manager, point_spacing=16, generate_normal=True)
source = cutout_source(size=(4096, 2048))
cutout._open_shape_pie(
interaction.open_shape_pie(
context,
object(),
source,
@@ -95,22 +93,30 @@ def test_cutout_pie_passes_image_lasso_options_to_every_shape():
points=((0, 0), (10, 0), (0, 10)),
invert=True,
),
refine_selection=True,
refine_region=True,
)
assert all(call[2].refine_selection for call in window_manager.pie.calls)
assert all(call[2].normal_mode == "OBJECT" for call in window_manager.pie.calls)
assert all(call[2].refine_region for call in window_manager.pie.calls)
assert all(call[2].generate_normal for call in window_manager.pie.calls)
assert all(
RegionPath.from_json(call[2].region_path_json).invert
for call in window_manager.pie.calls
)
assert all(call[2].mesh_detail == 256 for call in window_manager.pie.calls)
assert all(call[2].mesh_resolution == 256 for call in window_manager.pie.calls)
def test_cutout_normal_space_is_derived_from_shape():
assert cutout.normal_mode_for_shape("DEPTH_SURFACE", False) == "NONE"
assert cutout.normal_mode_for_shape("SURFACE", True) == "TANGENT"
assert cutout.normal_mode_for_shape("BALLOON", True) == "TANGENT"
assert cutout.normal_mode_for_shape("DEPTH_BALLOON", True) == "TANGENT"
assert cutout.normal_mode_for_shape("DEPTH_SURFACE", True) == "OBJECT"
def test_cutout_mesh_detail_tracks_the_source_long_edge_and_clamps():
assert cutout.cutout_mesh_detail((4096, 2048), 32) == 128
assert cutout.cutout_mesh_detail((16, 8), 4) == 4
assert cutout.cutout_mesh_detail((65536, 32768), 4) == 1024
assert interaction.cutout_mesh_resolution((4096, 2048), 32) == 128
assert interaction.cutout_mesh_resolution((16, 8), 4) == 4
assert interaction.cutout_mesh_resolution((65536, 32768), 4) == 1024
def test_cutout_reuses_image_lasso_gesture_and_double_click_selects_all(monkeypatch):
@@ -128,11 +134,11 @@ def test_cutout_reuses_image_lasso_gesture_and_double_click_selects_all(monkeypa
)
monkeypatch.setattr(cutout, "video_total_frames", lambda _image: 1)
monkeypatch.setattr(cutout, "apply_region_settings", lambda *_args: None)
monkeypatch.setattr(cutout, "_validate_cutout_region", lambda *_args: None)
monkeypatch.setattr(cutout, "validate_cutout_region", lambda *_args: None)
operator = SimpleNamespace(
full_image=True,
refine_selection=False,
refine_region=False,
report=lambda *_args: None,
)
operator.execute = MethodType(cutout.SelectCutoutRegion.execute, operator)
@@ -142,11 +148,9 @@ def test_cutout_reuses_image_lasso_gesture_and_double_click_selects_all(monkeypa
assert issubclass(cutout.SelectCutoutRegion, ImageRegionGesture)
assert result == {"FINISHED"}
region_path = RegionPath.from_json(
window_manager.pie.calls[0][2].region_path_json
)
region_path = RegionPath.from_json(window_manager.pie.calls[0][2].region_path_json)
assert region_path.points == ((0, 0), (320, 0), (320, 180), (0, 180))
assert window_manager.pie.calls[0][2].mesh_detail == 10
assert window_manager.pie.calls[0][2].mesh_resolution == 10
assert not hasattr(cutout, "pending_selection")