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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user