feat(selection): separate selection geometry from image content processing
- Introduced a new design for selection rasterization that decouples the selection geometry from the image content, ensuring that the SelectionMask is independent of the source image's alpha and color. - Updated the `rasterize_selection_path` function to only accept selection paths and image dimensions, removing parameters related to source alpha and alpha thresholds. - Implemented a new approach for handling selection paths using non-zero winding fill, ensuring overlapping areas are preserved. - Enhanced performance by utilizing a scanline algorithm for rasterization, significantly reducing processing time for high-resolution images. - Updated internal documentation and added comprehensive tests to validate the new behavior and performance improvements. - Ensured backward compatibility by maintaining existing interfaces where necessary while introducing breaking changes in the selection processing logic.
This commit is contained in:
@@ -1,9 +1,8 @@
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from types import MethodType, SimpleNamespace
|
||||
|
||||
import bpy
|
||||
|
||||
SOURCE_ROOT = Path(__file__).parents[1] / "src"
|
||||
sys.path.insert(0, str(SOURCE_ROOT))
|
||||
|
||||
@@ -83,6 +82,15 @@ def test_cutout_pie_passes_image_lasso_options_to_all_ai_shapes(monkeypatch):
|
||||
context = cutout_context(window_manager, mesh_detail="MEDIUM", generate_normal=True)
|
||||
source = cutout_source(size=(4096, 2048))
|
||||
monkeypatch.setattr(interaction, "ai_ready", lambda: True)
|
||||
serialized = []
|
||||
original_to_json = SelectionPath.to_json
|
||||
|
||||
def record_to_json(selection_path):
|
||||
value = original_to_json(selection_path)
|
||||
serialized.append(value)
|
||||
return value
|
||||
|
||||
monkeypatch.setattr(SelectionPath, "to_json", record_to_json)
|
||||
|
||||
interaction.open_shape_pie(
|
||||
context,
|
||||
@@ -103,6 +111,12 @@ def test_cutout_pie_passes_image_lasso_options_to_all_ai_shapes(monkeypatch):
|
||||
]
|
||||
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 len(serialized) == 1
|
||||
assert set(json.loads(serialized[0])) == {"points", "invert"}
|
||||
assert all(
|
||||
call[2].selection_path_json == serialized[0]
|
||||
for call in window_manager.pie.calls
|
||||
)
|
||||
assert all(
|
||||
SelectionPath.from_json(call[2].selection_path_json).invert
|
||||
for call in window_manager.pie.calls
|
||||
@@ -132,12 +146,7 @@ def test_cutout_mesh_detail_maps_to_denser_sampling():
|
||||
|
||||
|
||||
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)
|
||||
source = bpy.data.objects.new("Source", None)
|
||||
source.empty_display_type = "IMAGE"
|
||||
source.data = image
|
||||
bpy.context.collection.objects.link(source)
|
||||
source = cutout_source(size=(65536, 32768))
|
||||
window_manager = WindowManager()
|
||||
context = cutout_context(window_manager)
|
||||
|
||||
@@ -145,15 +154,32 @@ 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, "require_image_empty", lambda _name: source)
|
||||
monkeypatch.setattr(cutout_main, "apply_selection_settings", lambda *_args: None)
|
||||
monkeypatch.setattr(cutout_main, "validate_cutout_selection", lambda *_args: None)
|
||||
monkeypatch.setattr(cutout_main, "serialize_matrix", lambda _matrix: "matrix")
|
||||
monkeypatch.setattr(
|
||||
cutout_main,
|
||||
"image_rgba",
|
||||
lambda *_args: (_ for _ in ()).throw(AssertionError("unexpected pixel read")),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
cutout_main,
|
||||
"rasterize_selection_path",
|
||||
lambda *_args, **_kwargs: (_ for _ in ()).throw(
|
||||
AssertionError("unexpected Selection rasterization")
|
||||
),
|
||||
)
|
||||
|
||||
operator = SimpleNamespace(
|
||||
full_image=True,
|
||||
refine_selection=False,
|
||||
report=lambda *_args: None,
|
||||
)
|
||||
operator.execute = MethodType(cutout_main.SelectCutoutSelection.execute, operator)
|
||||
source.matrix_world = object()
|
||||
operator._submit_selection_path = MethodType(
|
||||
cutout_main.SelectCutoutSelection._submit_selection_path,
|
||||
operator,
|
||||
)
|
||||
result = cutout_main.SelectCutoutSelection.invoke(
|
||||
operator, context, SimpleNamespace(value="DOUBLE_CLICK")
|
||||
)
|
||||
@@ -161,8 +187,13 @@ def test_cutout_reuses_image_lasso_gesture_and_double_click_selects_all(monkeypa
|
||||
assert issubclass(cutout_main.SelectCutoutSelection, ImageSelectionGesture)
|
||||
assert result == {"FINISHED"}
|
||||
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 selection_path.points == (
|
||||
(0, 0),
|
||||
(65536, 0),
|
||||
(65536, 32768),
|
||||
(0, 32768),
|
||||
)
|
||||
assert window_manager.pie.calls[0][2].mesh_resolution == 1024
|
||||
assert not hasattr(cutout_main, "pending_selection")
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user