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:
2026-08-31 22:10:05 +08:00
parent 14d63001d2
commit c97b4a3e41
76 changed files with 2127 additions and 1137 deletions

View File

@@ -7,8 +7,8 @@ import bpy
SOURCE_ROOT = Path(__file__).parents[1] / "src"
sys.path.insert(0, str(SOURCE_ROOT))
from anyimage.common.image_interaction import ImageRegionGesture
from anyimage.common.image_region import RegionPath
from anyimage.common.selection import SelectionPath
from anyimage.common.viewport import ImageSelectionGesture
from anyimage.operators.cutout_tool import interaction
from anyimage.operators.cutout_tool import main as cutout_main
@@ -32,9 +32,9 @@ class WindowManager:
draw(menu, None)
def cutout_context(window_manager, point_spacing=32, generate_normal=False):
def cutout_context(window_manager, mesh_detail="LOW", generate_normal=False):
settings = SimpleNamespace(
cutout_point_spacing=point_spacing,
cutout_mesh_detail=mesh_detail,
cutout_generate_normal=generate_normal,
)
return SimpleNamespace(
@@ -57,8 +57,8 @@ def test_cutout_pie_uses_two_local_shapes_without_ai(monkeypatch):
context,
object(),
source,
RegionPath(points=((0, 0), (10, 0), (0, 10))),
refine_region=True,
SelectionPath(points=((0, 0), (10, 0), (0, 10))),
refine_selection=True,
)
assert [call[2].shape for call in window_manager.pie.calls] == [
@@ -70,17 +70,17 @@ def test_cutout_pie_uses_two_local_shapes_without_ai(monkeypatch):
"Balloon",
]
assert all(
call[0] == "anyimage.cutout_region_to_shape"
call[0] == "anyimage.cutout_selection_to_shape"
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].refine_selection 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_all_ai_shapes(monkeypatch):
window_manager = WindowManager()
context = cutout_context(window_manager, point_spacing=16, generate_normal=True)
context = cutout_context(window_manager, mesh_detail="MEDIUM", generate_normal=True)
source = cutout_source(size=(4096, 2048))
monkeypatch.setattr(interaction, "ai_ready", lambda: True)
@@ -88,11 +88,11 @@ def test_cutout_pie_passes_image_lasso_options_to_all_ai_shapes(monkeypatch):
context,
object(),
source,
RegionPath(
SelectionPath(
points=((0, 0), (10, 0), (0, 10)),
invert=True,
),
refine_region=True,
refine_selection=True,
)
assert [call[2].shape for call in window_manager.pie.calls] == [
@@ -101,10 +101,10 @@ def test_cutout_pie_passes_image_lasso_options_to_all_ai_shapes(monkeypatch):
"DEPTH_BALLOON",
"DEPTH_SURFACE",
]
assert all(call[2].refine_region for call in window_manager.pie.calls)
assert all(call[2].refine_selection 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
SelectionPath.from_json(call[2].selection_path_json).invert
for call in window_manager.pie.calls
)
assert all(call[2].mesh_resolution == 256 for call in window_manager.pie.calls)
@@ -124,6 +124,13 @@ def test_cutout_mesh_detail_tracks_the_source_long_edge_and_clamps():
assert interaction.cutout_mesh_resolution((65536, 32768), 4) == 1024
def test_cutout_mesh_detail_maps_to_denser_sampling():
assert [
interaction.cutout_mesh_detail_spacing(detail)
for detail in ("LOW", "MEDIUM", "HIGH", "ULTRA")
] == [32, 16, 8, 4]
def test_cutout_reuses_image_lasso_gesture_and_double_click_selects_all(monkeypatch):
bpy.ops.wm.read_factory_settings(use_empty=True)
image = bpy.data.images.new("Source", width=320, height=180)
@@ -138,23 +145,23 @@ def test_cutout_reuses_image_lasso_gesture_and_double_click_selects_all(monkeypa
cutout_main, "resolve_image_edit_click", lambda *_args: (source, True)
)
monkeypatch.setattr(cutout_main, "video_total_frames", lambda _image: 1)
monkeypatch.setattr(cutout_main, "apply_region_settings", lambda *_args: None)
monkeypatch.setattr(cutout_main, "validate_cutout_region", lambda *_args: None)
monkeypatch.setattr(cutout_main, "apply_selection_settings", lambda *_args: None)
monkeypatch.setattr(cutout_main, "validate_cutout_selection", lambda *_args: None)
operator = SimpleNamespace(
full_image=True,
refine_region=False,
refine_selection=False,
report=lambda *_args: None,
)
operator.execute = MethodType(cutout_main.SelectCutoutRegion.execute, operator)
result = cutout_main.SelectCutoutRegion.invoke(
operator.execute = MethodType(cutout_main.SelectCutoutSelection.execute, operator)
result = cutout_main.SelectCutoutSelection.invoke(
operator, context, SimpleNamespace(value="DOUBLE_CLICK")
)
assert issubclass(cutout_main.SelectCutoutRegion, ImageRegionGesture)
assert issubclass(cutout_main.SelectCutoutSelection, ImageSelectionGesture)
assert result == {"FINISHED"}
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))
selection_path = SelectionPath.from_json(window_manager.pie.calls[0][2].selection_path_json)
assert selection_path.points == ((0, 0), (320, 0), (320, 180), (0, 180))
assert window_manager.pie.calls[0][2].mesh_resolution == 10
assert not hasattr(cutout_main, "pending_selection")