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:
2026-09-01 22:36:20 +08:00
parent 2ff7b04f56
commit 7f6e47432e
54 changed files with 2831 additions and 634 deletions

View File

@@ -283,7 +283,7 @@ def test_selection_contours_group_holes_with_their_connected_component():
alpha[5:15, 5:15] = 255
selection_contours = extract_selection_contours(
SelectionMask(alpha, (0, 0, 200, 120)),
alpha,
max_resolution=200,
)
@@ -306,7 +306,7 @@ def test_selection_contours_accept_blender_float_mask_with_a_hole():
alpha[45:75, 80:120] = 0.0
selection_contours = extract_selection_contours(
SelectionMask(alpha, (0, 0, 200, 120)),
alpha,
max_resolution=200,
)
@@ -337,7 +337,7 @@ def test_depth_surface_cutout_uses_half_alpha_boundary():
alpha[4:8, 5:11] = 0.51
selection_contours = cutout_geometry.extract_cutout_contours(
SelectionMask(alpha, (0, 0, 16, 12)),
alpha,
64,
threshold=0.5,
)
@@ -364,7 +364,7 @@ def test_blender_cutout_alpha_pipeline_does_not_import_pillow():
)
antialiased = selection_mask.full_values((48, 32))
selection_contours = extract_selection_contours(
SelectionMask(antialiased, (0, 0, 48, 32)),
antialiased,
max_resolution=24,
)
@@ -380,8 +380,8 @@ def test_blender_cutout_contour_resolution_tracks_mesh_detail():
)
selection_mask = SelectionMask(np.ones((2, 2), dtype=np.float32), (0, 0, 2, 2))
with patch.object(cutout_geometry, "extract_selection_contours", return_value=expected) as trace:
assert cutout_geometry.extract_cutout_contours(selection_mask, 32) == expected
assert cutout_geometry.extract_cutout_contours(selection_mask, 256) == expected
assert cutout_geometry.extract_cutout_contours(selection_mask.values, 32) == expected
assert cutout_geometry.extract_cutout_contours(selection_mask.values, 256) == expected
assert trace.call_args_list[0].kwargs["max_resolution"] == 512
assert trace.call_args_list[1].kwargs["max_resolution"] == 1024
@@ -580,7 +580,10 @@ def test_selection_calibration_uses_the_final_selection_mask():
(0, 0, 2, 2),
)
valid, transparent_fraction = cutout_geometry.selection_calibration(selection_mask, (2, 2))
valid, transparent_fraction = cutout_geometry.selection_calibration(
selection_mask.values,
(2, 2),
)
assert np.array_equal(valid, ((True, True), (True, False)))
assert transparent_fraction == 0.25