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

@@ -4,10 +4,7 @@ from enum import StrEnum
import bpy
from ...common.image import (
color_data_name,
image_rgba,
)
from ...common.image import color_data_name
from ...common.depth import camera_depth_values, depth_uniform_scale
from ...common.material import (
IMAGE_MATERIAL_NODE_GROUP_NAME,
@@ -84,8 +81,8 @@ def _assign_float_point_attribute(mesh, name, values):
attribute.data.foreach_set("value", np.asarray(values, dtype=np.float32))
def _cropped_color_image(source_object, selection_mask, source_rgba):
crop_bounds = selection_mask.bounds
def create_cropped_color_image(source_object, crop_bounds, source_rgba):
"""Create a packed color image for one local Cutout bounds rectangle."""
import numpy as np
source = source_object.data
@@ -95,17 +92,10 @@ def _cropped_color_image(source_object, selection_mask, source_rgba):
height = bottom - top
if width <= 0 or height <= 0:
raise ValueError("The Cutout crop dimensions must be positive")
cropped = np.zeros((height, width, 4), dtype=np.float32)
source_height, source_width = rgba.shape[:2]
source_left = max(0, left)
source_top = max(0, top)
source_right = min(source_width, right)
source_bottom = min(source_height, bottom)
if source_right > source_left and source_bottom > source_top:
cropped[
source_top - top : source_bottom - top,
source_left - left : source_right - left,
] = rgba[source_top:source_bottom, source_left:source_right]
if left < 0 or top < 0 or right > source_width or bottom > source_height:
raise ValueError("The Cutout crop is outside the source RGBA")
cropped = rgba[top:bottom, left:right]
image = bpy.data.images.new(
color_data_name(source_object),
width=width,
@@ -151,21 +141,22 @@ def create_shape_object(
source_object,
shape,
mesh_resolution,
selection_mask,
content_values,
content_bounds,
base_shape,
*,
depth_image=None,
depth_metadata=None,
normal_image=None,
normal_mode=NormalMode.NONE,
source_rgba=None,
color_image,
):
import numpy as np
normal_mode = NormalMode(normal_mode)
bounds = crop_plane_bounds(
source_object,
selection_mask.bounds,
content_bounds,
tuple(source_object.data.size),
)
crop_center = (
@@ -177,11 +168,6 @@ def create_shape_object(
cutout_sampling_spacing(source_object, mesh_resolution),
world_metric,
)
color_image = _cropped_color_image(
source_object,
selection_mask,
image_rgba(source_object.data) if source_rgba is None else source_rgba,
)
material = None
mesh = None
mesh_object = None
@@ -209,7 +195,7 @@ def create_shape_object(
raise ValueError("Depth Shape requires a Depth texture and metadata")
camera_depth, valid = camera_depth_values(depth_image)
calibration_valid, transparent_fraction = selection_calibration(
selection_mask, valid.shape
content_values, valid.shape
)
calibration_valid &= valid
uniform_scale = depth_uniform_scale(
@@ -311,6 +297,4 @@ def create_shape_object(
bpy.data.meshes.remove(mesh)
if material is not None and material.users == 0:
bpy.data.materials.remove(material)
if color_image.users == 0:
bpy.data.images.remove(color_image, do_unlink=True)
raise