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

@@ -5,11 +5,11 @@ from dataclasses import dataclass
from ...preferences import addon_preferences
from ...server.media.resolution import limited_image_size
from ...common.image_data import image_empty_bounds
from ...common.image import image_empty_bounds
@dataclass(frozen=True)
class RegionContours:
class SelectionContours:
components: tuple
image_size: tuple[int, int]
@@ -22,11 +22,11 @@ class RegionContours:
)
image_size = tuple(int(value) for value in self.image_size)
if not components or any(not component for component in components):
raise ValueError("The Region has no usable contour components")
raise ValueError("The Selection has no usable contour components")
if any(len(contour) < 3 for component in components for contour in component):
raise ValueError("A Region contour has fewer than three points")
raise ValueError("A Selection contour has fewer than three points")
if len(image_size) != 2 or min(image_size) <= 0:
raise ValueError("The Region contour image size is invalid")
raise ValueError("The Selection contour image size is invalid")
object.__setattr__(self, "components", components)
object.__setattr__(self, "image_size", image_size)
@@ -57,7 +57,7 @@ def _simplify_axis_aligned_contour(points):
return simplified
def _trace_region_contours(mask):
def _trace_selection_contours(mask):
import numpy as np
mask = np.asarray(mask, dtype=bool)
@@ -89,7 +89,7 @@ def _trace_region_contours(mask):
for y, x in np.argwhere(left):
add_edge((int(x), int(y) + 1), (int(x), int(y)))
if not edges:
raise ValueError("The Cutout region has no usable boundary")
raise ValueError("The Cutout selection has no usable boundary")
contours = []
edge_count = sum(len(values) for values in edges.values())
@@ -117,7 +117,7 @@ def _trace_region_contours(mask):
if len(contour) >= 3:
contours.append(contour)
if not contours:
raise ValueError("The Cutout region is too small")
raise ValueError("The Cutout selection is too small")
return contours
@@ -143,13 +143,13 @@ def _point_inside_contour(point, contour):
return inside
def _group_region_contours(mask):
contours = _trace_region_contours(mask)
def _group_selection_contours(mask):
contours = _trace_selection_contours(mask)
outer = [contour for contour in contours if _signed_contour_area(contour) > 0.0]
holes = [contour for contour in contours if _signed_contour_area(contour) < 0.0]
if not outer:
raise ValueError("The Cutout region has no outer boundary")
regions = [[contour] for contour in outer]
raise ValueError("The Cutout selection has no outer boundary")
components = [[contour] for contour in outer]
outer_areas = [abs(_signed_contour_area(contour)) for contour in outer]
for hole in holes:
start = hole[0]
@@ -167,10 +167,10 @@ def _group_region_contours(mask):
if _point_inside_contour(sample, contour)
]
if not owners:
raise ValueError("The Cutout hole has no enclosing region")
raise ValueError("The Cutout hole has no enclosing selection component")
owner = min(owners, key=outer_areas.__getitem__)
regions[owner].append(hole)
return regions
components[owner].append(hole)
return components
def _resize_values(values, size):
@@ -202,10 +202,10 @@ def _resize_values(values, size):
)
def extract_region_contours(region_mask, max_resolution=512, threshold=0.5):
def extract_selection_contours(selection_mask, max_resolution=512, threshold=0.5):
import numpy as np
values = np.asarray(region_mask.values)
values = np.asarray(selection_mask.values)
if values.ndim != 2:
raise ValueError("Cutout Alpha must be two-dimensional")
if np.issubdtype(values.dtype, np.floating):
@@ -218,10 +218,10 @@ def extract_region_contours(region_mask, max_resolution=512, threshold=0.5):
if sampled_size != source_size:
values = _resize_values(values, sampled_size)
mask = values >= 255.0 * threshold
components = _group_region_contours(mask)
components = _group_selection_contours(mask)
scale_x = source_size[0] / sampled_size[0]
scale_y = source_size[1] / sampled_size[1]
return RegionContours(
return SelectionContours(
components=tuple(
tuple(
tuple(
@@ -257,17 +257,17 @@ def resample_closed_contour(contour, spacing):
points = np.asarray(contour, dtype=np.float64)
if len(points) < 3:
raise ValueError("The lasso does not define a valid image region")
raise ValueError("The lasso does not define a valid image selection")
segments = np.roll(points, -1, axis=0) - points
lengths = np.linalg.norm(segments, axis=1)
points = points[lengths > 1e-8]
if len(points) < 3:
raise ValueError("The lasso region is too small to create a Cutout")
raise ValueError("The selection is too small to create a Cutout")
segments = np.roll(points, -1, axis=0) - points
lengths = np.linalg.norm(segments, axis=1)
perimeter = float(lengths.sum())
if perimeter <= spacing * 3.0:
raise ValueError("The lasso region is too small to create a Cutout")
raise ValueError("The selection is too small to create a Cutout")
count = max(3, int(round(perimeter / spacing)))
distances = np.arange(count, dtype=np.float64) * perimeter / count
cumulative = np.concatenate(([0.0], np.cumsum(lengths)))
@@ -276,7 +276,7 @@ def resample_closed_contour(contour, spacing):
return points[indices] + segments[indices] * factors[:, None]
def smooth_region_contour(contour, spacing, iterations=16):
def smooth_selection_contour(contour, spacing, iterations=16):
contour = resample_closed_contour(contour, spacing * 0.25)
original_area = abs(contour_area(contour))
for _index in range(iterations):
@@ -509,30 +509,30 @@ def prepare_planar_contours(
if perimeter <= 1e-12:
continue
contour_spacing = min(spacing, perimeter / 8.0)
smoothed = smooth_region_contour(planar, contour_spacing)
smoothed = smooth_selection_contour(planar, contour_spacing)
smoothed_local = smoothed @ np.linalg.inv(transform)
smoothed_local[:, 0] = np.clip(smoothed_local[:, 0], x_min, x_max)
smoothed_local[:, 1] = np.clip(smoothed_local[:, 1], y_min, y_max)
prepared.append(smoothed_local @ transform)
if not prepared:
raise ValueError("The Cutout has no region large enough for this resolution")
raise ValueError("The Cutout has no selection large enough for this resolution")
interior = uniform_interior_points_for_contours(prepared, spacing)
return prepared, interior, spacing
def _prepare_region_contours(
region_contours,
def _prepare_selection_contours(
selection_contours,
local_bounds,
spacing=POINT_SPACING,
world_metric=None,
):
prepared = []
errors = []
for component in region_contours.components:
for component in selection_contours.components:
try:
contours, interior, _spacing = prepare_planar_contours(
component,
region_contours.image_size,
selection_contours.image_size,
local_bounds,
spacing=spacing,
world_metric=world_metric,
@@ -544,7 +544,7 @@ def _prepare_region_contours(
if not prepared:
if errors:
raise errors[0]
raise ValueError("The Cutout has no usable region")
raise ValueError("The Cutout has no usable selection")
return prepared, spacing
@@ -582,7 +582,7 @@ def _triangulate_contour_component(contours, interior, spacing):
centers = planar_points[faces].mean(axis=1)
faces = faces[points_inside_contours(centers, contours)]
if not len(faces):
raise ValueError("Unable to triangulate the Cutout regions")
raise ValueError("Unable to triangulate the Cutout selection components")
used = np.unique(faces)
if len(used) != len(planar_points):
remap = np.full(len(planar_points), -1, dtype=np.int32)
@@ -614,8 +614,8 @@ def boundary_vertices_from_faces(vertex_count, faces):
return boundary
def triangulate_region_contours(
region_contours,
def triangulate_selection_contours(
selection_contours,
local_bounds,
spacing=POINT_SPACING,
world_metric=None,
@@ -623,8 +623,8 @@ def triangulate_region_contours(
):
import numpy as np
prepared, spacing = _prepare_region_contours(
region_contours,
prepared, spacing = _prepare_selection_contours(
selection_contours,
local_bounds,
spacing=spacing,
world_metric=world_metric,
@@ -635,7 +635,7 @@ def triangulate_region_contours(
components = []
offset = 0
for contours, interior in prepared:
region_points, region_faces, region_boundary = _triangulate_contour_component(
component_points, component_faces, component_boundary = _triangulate_contour_component(
contours,
interior,
spacing,
@@ -643,17 +643,17 @@ def triangulate_region_contours(
components.append(
(
offset,
offset + len(region_points),
region_faces,
offset + len(component_points),
component_faces,
contours[0],
)
)
points.append(region_points)
faces.extend(tuple(offset + value for value in face) for face in region_faces)
boundaries.append(region_boundary)
offset += len(region_points)
points.append(component_points)
faces.extend(tuple(offset + value for value in face) for face in component_faces)
boundaries.append(component_boundary)
offset += len(component_points)
if not points:
raise ValueError("Unable to triangulate the Cutout regions")
raise ValueError("Unable to triangulate the Cutout selection components")
result = (
np.concatenate(points, axis=0),
faces,
@@ -1065,9 +1065,9 @@ DEFAULT_DEPTH_BASELINE = 8.0
DEFAULT_DEPTH_DIRECTION = (0.0, 0.0, 1.0)
def use_adaptive_depth_calibration(region_transparent_fraction):
"""Return whether the Region input has enough transparent context."""
return float(region_transparent_fraction) >= ADAPTIVE_DEPTH_TRANSPARENCY_RATIO
def use_adaptive_depth_calibration(selection_transparent_fraction):
"""Return whether the Selection input has enough transparent context."""
return float(selection_transparent_fraction) >= ADAPTIVE_DEPTH_TRANSPARENCY_RATIO
def metric_depth_reference(camera_depth, valid, uniform_scale):
@@ -1253,7 +1253,7 @@ def orient_cutout_vertices(vertices):
def cutout_domain(
region_contours,
selection_contours,
bounds,
spacing,
world_metric,
@@ -1263,8 +1263,8 @@ def cutout_domain(
x_min, x_max, y_min, y_max = bounds
planar, planar_faces, boundary, components = triangulate_region_contours(
region_contours,
planar, planar_faces, boundary, components = triangulate_selection_contours(
selection_contours,
bounds,
spacing=spacing,
world_metric=world_metric,
@@ -1341,13 +1341,13 @@ def crop_plane_bounds(source_object, crop_bounds, source_size):
def extract_cutout_contours(
region_mask,
selection_mask,
mesh_resolution,
threshold=CUTOUT_ALPHA_THRESHOLD,
):
contour_resolution = max(512, min(int(mesh_resolution) * 8, 1024))
return extract_region_contours(
region_mask,
return extract_selection_contours(
selection_mask,
max_resolution=contour_resolution,
threshold=threshold,
)
@@ -1363,7 +1363,7 @@ def limit_cutout_mesh_resolution(mesh_resolution, crop_size, max_input_size):
return max(4, min(1024, round(int(mesh_resolution) * scale)))
def cutout_point_spacing(source_object, mesh_resolution):
def cutout_sampling_spacing(source_object, mesh_resolution):
import math
x_min, x_max, y_min, y_max = image_empty_bounds(source_object)
@@ -1373,11 +1373,11 @@ def cutout_point_spacing(source_object, mesh_resolution):
return max(width, height) / max(int(mesh_resolution), 8)
def region_calibration(region_mask, target_shape, threshold=CUTOUT_ALPHA_THRESHOLD):
"""Return the final Region mask and transparency ratio at geometry resolution."""
def selection_calibration(selection_mask, target_shape, threshold=CUTOUT_ALPHA_THRESHOLD):
"""Return the final Selection mask and transparency ratio at geometry resolution."""
import numpy as np
values = np.asarray(region_mask.values, dtype=np.float32)
values = np.asarray(selection_mask.values, dtype=np.float32)
target_height, target_width = (int(value) for value in target_shape)
source_height, source_width = values.shape
y = np.minimum(
@@ -1394,16 +1394,16 @@ def region_calibration(region_mask, target_shape, threshold=CUTOUT_ALPHA_THRESHO
return valid, transparent_fraction
def build_base_shape(context, source_object, mesh_resolution, region_mask):
def build_base_shape(context, source_object, mesh_resolution, selection_mask):
import numpy as np
bounds = crop_plane_bounds(
source_object,
region_mask.bounds,
selection_mask.bounds,
tuple(source_object.data.size),
)
region_contours = extract_cutout_contours(
region_mask,
selection_contours = extract_cutout_contours(
selection_mask,
mesh_resolution,
threshold=CUTOUT_ALPHA_THRESHOLD,
)
@@ -1412,9 +1412,9 @@ def build_base_shape(context, source_object, mesh_resolution, region_mask):
raise ValueError("The Image Empty has a degenerate world transform")
preferences = addon_preferences(context)
balloon_algorithm = getattr(preferences, "balloon_profile_algorithm", "POISSON")
point_spacing = cutout_point_spacing(source_object, mesh_resolution)
point_spacing = cutout_sampling_spacing(source_object, mesh_resolution)
domain = cutout_domain(
region_contours,
selection_contours,
bounds,
point_spacing,
world_metric,