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

@@ -1,7 +1,7 @@
from .remove_background import RemoveImageBackground, RunBackgroundRemoval
from .cutout_tool import (
CutoutRegionToShape,
SelectCutoutRegion,
CutoutSelectionToShape,
SelectCutoutSelection,
)
from .debug import DebugBen2, DebugDa3, DebugMoge2, DebugUpscale
from .image_tool import CLASSES as IMAGE_TOOL_CLASSES
@@ -31,8 +31,8 @@ CLASSES = (
DebugBen2,
DebugUpscale,
GenerateDepthPlane,
CutoutRegionToShape,
SelectCutoutRegion,
CutoutSelectionToShape,
SelectCutoutSelection,
RunBackgroundRemoval,
ClearModels,
*IMAGE_TOOL_CLASSES,

View File

@@ -8,7 +8,7 @@ from ..properties import (
model_download_size,
model_record,
)
from ..common.ai_job import require_environment
from ..common.ai import require_environment
def format_download_size(size):

View File

@@ -8,6 +8,7 @@ import bpy
CLIPBOARD_HASH_PROPERTY = "anyimage_clipboard_sha256"
CLIPBOARD_TEXTURE_PROPERTY = "anyimage_clipboard_texture"
DEFAULT_STENCIL_SIZE = 256.0
REFERENCE_IMAGE_SIZE = 5.0
NODE_HEADER_HEIGHT = 20.0
NODE_TYPES = {
"CompositorNodeTree": "CompositorNodeImage",
@@ -173,7 +174,7 @@ def add_reference_image(context, image, location=None):
reference = bpy.data.objects.new(image.name, None)
reference.empty_display_type = "IMAGE"
reference.data = image
reference.empty_display_size = 2.0
reference.empty_display_size = REFERENCE_IMAGE_SIZE
context.collection.objects.link(reference)
_place_in_view(context, reference, location)
_select_only(context, reference)

View File

@@ -8,7 +8,7 @@ from ...server.media.resolution import (
MAX_MAX_AI_INPUT_SIZE,
MIN_MAX_AI_INPUT_SIZE,
)
from ...common.ai_job import (
from ...common.ai import (
invoke_ai_setup_if_needed,
moge2_parameters,
production_device,
@@ -16,7 +16,7 @@ from ...common.ai_job import (
require_input_path,
require_model,
)
from ...common.image_data import (
from ...common.image import (
cleanup_image_input,
image_user_settings,
is_image_empty,
@@ -24,7 +24,7 @@ from ...common.image_data import (
source_start_frame,
video_total_frames,
)
from ...common.object import create_image_material
from ...common.material import create_image_material
from .object import (
create_color_image,
create_depth_plane_from_result,

View File

@@ -2,20 +2,20 @@
import bpy
from ...common.image_data import (
from ...common.image import (
color_data_name,
image_empty_bounds,
image_pixels,
load_color_result_image,
require_image_empty,
)
from ...common.depth_data import (
from ...common.depth import (
depth_uniform_scale,
load_depth_metadata,
load_depth_result_image,
)
from ...common.material import create_image_material
from ...common.object import (
create_image_material,
finalize_object_result,
modifier_input_identifier,
set_modifier_input,

View File

@@ -1,7 +1,7 @@
from .main import CutoutRegionToShape, CutoutTool, SelectCutoutRegion
from .main import CutoutSelectionToShape, CutoutTool, SelectCutoutSelection
__all__ = (
"CutoutRegionToShape",
"CutoutSelectionToShape",
"CutoutTool",
"SelectCutoutRegion",
"SelectCutoutSelection",
)

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,

View File

@@ -1,12 +1,12 @@
"""Cutout Region validation and Shape-menu interaction helpers."""
"""Cutout Selection validation and Shape-menu interaction helpers."""
from ...preferences import (
MAX_CUTOUT_MESH_RESOLUTION,
MIN_CUTOUT_MESH_RESOLUTION,
)
from ...properties import ai_ready
from ...common.image_data import image_rgba
from ...common.image_region import rasterize_region_path
from ...common.image import image_rgba
from ...common.selection import rasterize_selection_path
from .geometry import CUTOUT_ALPHA_THRESHOLD
from .shape import CUTOUT_SHAPE_LABELS
@@ -17,16 +17,23 @@ CUTOUT_SHAPE_BUTTONS = (
("DEPTH_SURFACE", "SURFACE_NCURVE"),
)
CUTOUT_MESH_DETAIL_SPACING = {
"LOW": 32,
"MEDIUM": 16,
"HIGH": 8,
"ULTRA": 4,
}
def validate_cutout_region(
def validate_cutout_selection(
source_object,
region_path,
selection_path,
):
image = source_object.data
rgba = image_rgba(image)
rasterize_region_path(
rasterize_selection_path(
tuple(image.size),
region_path,
selection_path,
source_alpha=rgba[:, :, 3],
alpha_threshold=CUTOUT_ALPHA_THRESHOLD,
antialias=True,
@@ -42,26 +49,30 @@ def cutout_mesh_resolution(image_size, point_spacing):
)
def cutout_mesh_detail_spacing(mesh_detail):
return CUTOUT_MESH_DETAIL_SPACING[str(mesh_detail)]
def _pie_button(
pie,
shape,
label,
icon,
source_name,
region_path_json,
refine_region,
selection_path_json,
refine_selection,
generate_normal,
mesh_resolution,
):
operator = pie.operator(
"anyimage.cutout_region_to_shape",
"anyimage.cutout_selection_to_shape",
text=label,
icon=icon,
)
operator.source_object_name = source_name
operator.region_path_json = region_path_json
operator.selection_path_json = selection_path_json
operator.shape = shape
operator.refine_region = refine_region
operator.refine_selection = refine_selection
operator.generate_normal = generate_normal
operator.mesh_resolution = mesh_resolution
@@ -70,15 +81,15 @@ def open_shape_pie(
context,
event,
source_object,
region_path,
refine_region=False,
selection_path,
refine_selection=False,
):
source_name = source_object.name
region_path_json = region_path.to_json()
selection_path_json = selection_path.to_json()
settings = context.scene.anyimage_settings
point_spacing = settings.cutout_point_spacing
point_spacing = cutout_mesh_detail_spacing(settings.cutout_mesh_detail)
ready = ai_ready()
refine_region = ready and refine_region
refine_selection = ready and refine_selection
generate_normal = ready and bool(
getattr(settings, "cutout_generate_normal", False)
)
@@ -94,8 +105,8 @@ def open_shape_pie(
CUTOUT_SHAPE_LABELS[shape],
icon,
source_name,
region_path_json,
refine_region,
selection_path_json,
refine_selection,
generate_normal,
mesh_resolution,
)

View File

@@ -1,4 +1,4 @@
"""Convert one image Region into a Cutout Shape."""
"""Convert one image Selection into a Cutout Shape."""
import json
@@ -14,7 +14,7 @@ from ...preferences import (
from ...properties import (
BEN2_MODEL_KEY,
DEFAULT_MOGE2_MODEL_KEY,
refine_region_property,
refine_selection_property,
)
from ...runtime import JobOperatorBase
from ...server.media.resolution import (
@@ -22,7 +22,9 @@ from ...server.media.resolution import (
MAX_MAX_AI_INPUT_SIZE,
MIN_MAX_AI_INPUT_SIZE,
)
from ...common.ai_job import (
from ...common.ai import (
draw_ai_property,
draw_ai_setup,
invoke_ai_setup_if_needed,
moge2_parameters,
production_device,
@@ -30,38 +32,36 @@ from ...common.ai_job import (
require_input_path,
require_model,
)
from ...common.image_data import (
from ...common.image import (
cleanup_image_input,
image_rgba,
load_normal_result_image,
prepare_region_input,
prepare_selection_input,
require_image_empty,
source_start_frame,
video_total_frames,
)
from ...common.depth_data import load_depth_metadata, load_depth_result_image
from ...common.image_interaction import (
ImageRegionGesture,
apply_region_settings,
draw_ai_property,
draw_ai_setup,
from ...common.depth import load_depth_metadata, load_depth_result_image
from ...common.viewport import (
ImageSelectionGesture,
apply_selection_settings,
gesture_property,
report_image_edit_exception,
resolve_image_edit_click,
serialize_matrix,
)
from ...common.image_region import (
from ...common.selection import (
ImageEditWarning,
RegionMask,
RegionPath,
rasterize_region_path,
SelectionMask,
SelectionPath,
rasterize_selection_path,
)
from .geometry import (
CUTOUT_ALPHA_THRESHOLD,
build_base_shape,
limit_cutout_mesh_resolution,
)
from .interaction import open_shape_pie, validate_cutout_region
from .interaction import open_shape_pie, validate_cutout_selection
from .object import NormalMode, create_shape_object
from .shape import CUTOUT_SHAPES, DEPTH_CUTOUT_SHAPES
@@ -89,7 +89,7 @@ def create_cutout_shape(
source_object,
shape,
mesh_resolution,
region_mask,
selection_mask,
*,
depth_image=None,
depth_metadata=None,
@@ -101,14 +101,14 @@ def create_cutout_shape(
context,
source_object,
mesh_resolution,
region_mask,
selection_mask,
)
result = create_shape_object(
context,
source_object,
shape,
mesh_resolution,
region_mask,
selection_mask,
base_shape,
depth_image=depth_image,
depth_metadata=depth_metadata,
@@ -122,10 +122,10 @@ def create_cutout_shape(
return result
def _region_mask_from_image(path, bounds):
def _selection_mask_from_image(path, bounds):
image = bpy.data.images.load(str(path), check_existing=False)
try:
return RegionMask(
return SelectionMask(
image_rgba(image)[:, :, 3],
tuple(int(value) for value in bounds),
)
@@ -133,13 +133,13 @@ def _region_mask_from_image(path, bounds):
bpy.data.images.remove(image, do_unlink=True)
def _generated_region_mask(result, input_path, source_bounds):
if not isinstance(result.value, dict) or "region_image" not in result.value:
return _region_mask_from_image(input_path, source_bounds)
def _generated_selection_mask(result, input_path, source_bounds):
if not isinstance(result.value, dict) or "selection_image" not in result.value:
return _selection_mask_from_image(input_path, source_bounds)
local_bounds = tuple(
int(value)
for value in json.loads(
result.file("region_bounds").read_text(encoding="utf-8")
result.file("selection_bounds").read_text(encoding="utf-8")
)
)
bounds = (
@@ -148,7 +148,7 @@ def _generated_region_mask(result, input_path, source_bounds):
source_bounds[0] + local_bounds[2],
source_bounds[1] + local_bounds[3],
)
return _region_mask_from_image(result.file("region_image"), bounds)
return _selection_mask_from_image(result.file("selection_image"), bounds)
def cutout_geometry_settings(context):
@@ -160,18 +160,18 @@ def cutout_geometry_settings(context):
)
class CutoutRegionToShape(JobOperatorBase, bpy.types.Operator):
bl_idname = "anyimage.cutout_region_to_shape"
bl_label = "Cutout Region to Shape"
bl_description = "Build the selected image Region as a Cutout shape"
class CutoutSelectionToShape(JobOperatorBase, bpy.types.Operator):
bl_idname = "anyimage.cutout_selection_to_shape"
bl_label = "Cutout Selection to Shape"
bl_description = "Build the image selection as a Cutout shape"
bl_options = {"INTERNAL"}
job_type = "generate-cutout-artifacts"
starting_message = "Generating Cutout artifacts"
input_path: bpy.props.StringProperty(options=_HIDDEN)
source_object_name: bpy.props.StringProperty(options=_HIDDEN)
region_path_json: bpy.props.StringProperty(options=_HIDDEN)
region_bounds_json: bpy.props.StringProperty(options=_HIDDEN)
selection_path_json: bpy.props.StringProperty(options=_HIDDEN)
selection_bounds_json: bpy.props.StringProperty(options=_HIDDEN)
shape: bpy.props.StringProperty(options=_HIDDEN)
mesh_resolution: bpy.props.IntProperty(
default=128,
@@ -196,11 +196,11 @@ class CutoutRegionToShape(JobOperatorBase, bpy.types.Operator):
options=_HIDDEN,
)
generate_normal: bpy.props.BoolProperty(default=False, options=_HIDDEN)
refine_region: bpy.props.BoolProperty(default=False, options=_HIDDEN)
refine_selection: bpy.props.BoolProperty(default=False, options=_HIDDEN)
def _needs_generation(self):
return (
self.refine_region
self.refine_selection
or self.shape in DEPTH_CUTOUT_SHAPES
or self.generate_normal
)
@@ -212,19 +212,19 @@ class CutoutRegionToShape(JobOperatorBase, bpy.types.Operator):
raise ValueError("Cutout currently supports still images")
if self.shape not in CUTOUT_SHAPES:
raise ValueError(f"Unsupported Cutout shape: {self.shape}")
region_path = RegionPath.from_json(self.region_path_json)
selection_path = SelectionPath.from_json(self.selection_path_json)
source_rgba = image_rgba(source_object.data)
region_mask = rasterize_region_path(
selection_mask = rasterize_selection_path(
tuple(source_object.data.size),
region_path,
selection_path,
source_alpha=source_rgba[:, :, 3],
alpha_threshold=CUTOUT_ALPHA_THRESHOLD,
antialias=True,
)
context.view_layer.objects.active = source_object
source_object.select_set(True)
crop_width = region_mask.bounds[2] - region_mask.bounds[0]
crop_height = region_mask.bounds[3] - region_mask.bounds[1]
crop_width = selection_mask.bounds[2] - selection_mask.bounds[0]
crop_height = selection_mask.bounds[3] - selection_mask.bounds[1]
self.mesh_resolution = limit_cutout_mesh_resolution(
self.mesh_resolution,
(crop_width, crop_height),
@@ -237,7 +237,7 @@ class CutoutRegionToShape(JobOperatorBase, bpy.types.Operator):
source_object,
self.shape,
self.mesh_resolution,
region_mask,
selection_mask,
source_rgba=source_rgba,
)
return {"FINISHED"}
@@ -245,11 +245,11 @@ class CutoutRegionToShape(JobOperatorBase, bpy.types.Operator):
if setup_result is not None:
return setup_result
self.model, self.resolution_level = cutout_geometry_settings(context)
self.region_bounds_json = json.dumps(
region_mask.bounds,
self.selection_bounds_json = json.dumps(
selection_mask.bounds,
separators=(",", ":"),
)
self.input_path = str(prepare_region_input(source_object.data, region_mask))
self.input_path = str(prepare_selection_input(source_object.data, selection_mask))
except (
ImageEditWarning,
OSError,
@@ -269,19 +269,19 @@ class CutoutRegionToShape(JobOperatorBase, bpy.types.Operator):
source_path = require_input_path(self.input_path)
depth_mode = depth_mode_for_shape(self.shape)
normal_mode = normal_mode_for_shape(self.shape, self.generate_normal)
if self.refine_region:
if self.refine_selection:
require_model(BEN2_MODEL_KEY)
if depth_mode != "NONE" or normal_mode != NormalMode.NONE:
require_model(self.model)
parameters = {
"input": str(source_path),
"device": production_device().lower(),
"refine_region": self.refine_region,
"refine_selection": self.refine_selection,
"depth_mode": depth_mode,
"normal_mode": normal_mode.value,
"max_input_size": int(self.max_ai_input_size),
}
if self.refine_region:
if self.refine_selection:
parameters["refine_model"] = BEN2_MODEL_KEY
if depth_mode != "NONE" or normal_mode != NormalMode.NONE:
start_frame = source_start_frame(self.source_object_name)
@@ -302,8 +302,8 @@ class CutoutRegionToShape(JobOperatorBase, bpy.types.Operator):
def response(self, context, result):
source_object = require_image_empty(self.source_object_name)
source_bounds = tuple(json.loads(self.region_bounds_json))
region_mask = _generated_region_mask(
source_bounds = tuple(json.loads(self.selection_bounds_json))
selection_mask = _generated_selection_mask(
result,
self.input_path,
source_bounds,
@@ -333,7 +333,7 @@ class CutoutRegionToShape(JobOperatorBase, bpy.types.Operator):
source_object,
self.shape,
self.mesh_resolution,
region_mask,
selection_mask,
depth_image=depth_image,
depth_metadata=depth_metadata,
normal_image=normal_image,
@@ -351,18 +351,18 @@ class CutoutRegionToShape(JobOperatorBase, bpy.types.Operator):
cleanup_image_input(self.input_path, True)
class SelectCutoutRegion(ImageRegionGesture, bpy.types.Operator):
bl_idname = "anyimage.select_cutout_region"
bl_label = "Select Cutout Region"
bl_description = "Lasso an Image Empty region, then choose its mesh shape"
class SelectCutoutSelection(ImageSelectionGesture, bpy.types.Operator):
bl_idname = "anyimage.select_cutout_selection"
bl_label = "Start Cutout Selection"
bl_description = "Lasso an Image Empty selection, then choose its mesh shape"
bl_options = {"INTERNAL"}
active_tool_id = "anyimage.cutout_lasso"
refine_region_settings = {"LASSO": "cutout_refine_region"}
refine_selection_settings = {"LASSO": "cutout_refine_selection"}
source_object_name: bpy.props.StringProperty(options={"HIDDEN", "SKIP_SAVE"})
source_matrix_data: bpy.props.StringProperty(options={"HIDDEN", "SKIP_SAVE"})
region_path_json: bpy.props.StringProperty(options={"HIDDEN", "SKIP_SAVE"})
refine_region: refine_region_property()
selection_path_json: bpy.props.StringProperty(options={"HIDDEN", "SKIP_SAVE"})
refine_selection: refine_selection_property()
invert: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"})
gesture: gesture_property()
full_image: bpy.props.BoolProperty(default=False, options={"HIDDEN", "SKIP_SAVE"})
@@ -391,12 +391,12 @@ class SelectCutoutRegion(ImageRegionGesture, bpy.types.Operator):
if video_total_frames(source_object.data) > 1:
self.report({"WARNING"}, "Image tools only support a still image")
return {"CANCELLED"}
apply_region_settings(self, context)
apply_selection_settings(self, context)
self.invert = False
self.source_object_name = source_object.name
self.source_matrix_data = serialize_matrix(source_object.matrix_world)
width, height = (int(value) for value in source_object.data.size)
self.region_path_json = RegionPath(
self.selection_path_json = SelectionPath(
points=((0, 0), (width, 0), (width, height), (0, height)),
fill_rule="NONZERO",
).to_json()
@@ -413,8 +413,8 @@ class SelectCutoutRegion(ImageRegionGesture, bpy.types.Operator):
self.report({"ERROR"}, str(error))
return {"CANCELLED"}
try:
region_path = RegionPath.from_json(self.region_path_json)
validate_cutout_region(source_object, region_path)
selection_path = SelectionPath.from_json(self.selection_path_json)
validate_cutout_selection(source_object, selection_path)
except (ImageEditWarning, TypeError, ValueError) as error:
report_image_edit_exception(self, error)
return {"CANCELLED"}
@@ -422,8 +422,8 @@ class SelectCutoutRegion(ImageRegionGesture, bpy.types.Operator):
context,
self._pie_event,
source_object,
region_path,
self.refine_region,
selection_path,
self.refine_selection,
)
return {"FINISHED"}
@@ -435,15 +435,15 @@ class CutoutTool(bpy.types.WorkSpaceTool):
bl_label = "Cutout"
bl_description = "Lasso an Image Empty and choose a mesh shape"
bl_icon = "ops.sculpt.lasso_mask"
bl_operator = SelectCutoutRegion.bl_idname
bl_operator = SelectCutoutSelection.bl_idname
bl_keymap = (
(
SelectCutoutRegion.bl_idname,
SelectCutoutSelection.bl_idname,
{"type": "LEFTMOUSE", "value": "DOUBLE_CLICK"},
{"properties": [("gesture", "LASSO"), ("full_image", True)]},
),
(
SelectCutoutRegion.bl_idname,
SelectCutoutSelection.bl_idname,
{"type": "LEFTMOUSE", "value": "PRESS"},
{"properties": [("gesture", "LASSO")]},
),
@@ -452,6 +452,6 @@ class CutoutTool(bpy.types.WorkSpaceTool):
def draw_settings(context, layout, _tool):
settings = context.scene.anyimage_settings
status = draw_ai_setup(layout)
draw_ai_property(layout, settings, "cutout_refine_region", status)
layout.prop(settings, "cutout_point_spacing")
draw_ai_property(layout, settings, "cutout_refine_selection", status)
draw_ai_property(layout, settings, "cutout_generate_normal", status)
layout.prop(settings, "cutout_mesh_detail")

View File

@@ -4,14 +4,16 @@ from enum import StrEnum
import bpy
from ...common.image_data import (
from ...common.image import (
color_data_name,
image_rgba,
)
from ...common.depth_data import camera_depth_values, depth_uniform_scale
from ...common.object import (
from ...common.depth import camera_depth_values, depth_uniform_scale
from ...common.material import (
IMAGE_MATERIAL_NODE_GROUP_NAME,
create_image_material,
)
from ...common.object import (
finalize_object_result,
modifier_input_identifier,
set_modifier_input,
@@ -22,10 +24,10 @@ from .geometry import (
DEFAULT_DEPTH_DIRECTION,
crop_plane_bounds,
cutout_cleanup_value,
cutout_point_spacing,
cutout_sampling_spacing,
depth_balloon_projection_parameters,
metric_depth_reference,
region_calibration,
selection_calibration,
use_adaptive_depth_calibration,
world_plane_metric,
)
@@ -82,8 +84,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, region_mask, source_rgba):
crop_bounds = region_mask.bounds
def _cropped_color_image(source_object, selection_mask, source_rgba):
crop_bounds = selection_mask.bounds
import numpy as np
source = source_object.data
@@ -149,7 +151,7 @@ def create_shape_object(
source_object,
shape,
mesh_resolution,
region_mask,
selection_mask,
base_shape,
*,
depth_image=None,
@@ -163,7 +165,7 @@ def create_shape_object(
normal_mode = NormalMode(normal_mode)
bounds = crop_plane_bounds(
source_object,
region_mask.bounds,
selection_mask.bounds,
tuple(source_object.data.size),
)
crop_center = (
@@ -172,12 +174,12 @@ def create_shape_object(
)
world_metric, _world_z_scale = world_plane_metric(source_object.matrix_world)
cleanup_value = cutout_cleanup_value(
cutout_point_spacing(source_object, mesh_resolution),
cutout_sampling_spacing(source_object, mesh_resolution),
world_metric,
)
color_image = _cropped_color_image(
source_object,
region_mask,
selection_mask,
image_rgba(source_object.data) if source_rgba is None else source_rgba,
)
material = None
@@ -206,8 +208,8 @@ def create_shape_object(
if depth_image is None or depth_metadata is None:
raise ValueError("Depth Shape requires a Depth texture and metadata")
camera_depth, valid = camera_depth_values(depth_image)
calibration_valid, transparent_fraction = region_calibration(
region_mask, valid.shape
calibration_valid, transparent_fraction = selection_calibration(
selection_mask, valid.shape
)
calibration_valid &= valid
uniform_scale = depth_uniform_scale(

View File

@@ -4,7 +4,7 @@ import bpy
from ..runtime import JobOperatorBase
from ..properties import BEN2_MODEL_KEY, REALESRGAN_GENERAL_WDN_MODEL_KEY
from ..common.ai_job import (
from ..common.ai import (
require_background_input_path,
da3_parameters,
require_input_path,

View File

@@ -1,12 +1,12 @@
from .box import ImageBoxTool
from .edit_mask import ActivateImageTool, EditImageMask, RefineImageRegion
from .edit_mask import ActivateImageTool, EditImageMask, RefineImageSelection
from .lasso import ImageLassoTool
from .line import ImageLineTool
from .polyline import ImagePolylineTool
CLASSES = (
EditImageMask,
RefineImageRegion,
RefineImageSelection,
ActivateImageTool,
)

View File

@@ -1,6 +1,6 @@
import bpy
from ...common.image_interaction import draw_region_settings
from ...common.viewport import draw_selection_settings
from .edit_mask import EditImageMask
@@ -21,4 +21,4 @@ class ImageBoxTool(bpy.types.WorkSpaceTool):
)
def draw_settings(context, layout, _tool):
draw_region_settings(context, layout, "image_box_refine_region")
draw_selection_settings(context, layout, "image_box_refine_selection")

View File

@@ -2,34 +2,34 @@ import json
import bpy
from ...properties import BEN2_MODEL_KEY, refine_region_property
from ...properties import BEN2_MODEL_KEY, refine_selection_property
from ...runtime import JobOperatorBase
from ...common.ai_job import (
from ...common.ai import (
production_device,
require_environment,
require_model,
)
from ...common.image_data import (
from ...common.image import (
cleanup_image_input,
create_pixel_result_image,
image_pixels,
image_rgba,
prepare_region_input,
prepare_selection_input,
replace_empty_image,
require_image_empty,
video_total_frames,
)
from ...common.image_interaction import (
ImageRegionGesture,
from ...common.viewport import (
ImageSelectionGesture,
gesture_property,
image_edit_poll,
report_image_edit_exception,
)
from ...common.image_region import (
from ...common.selection import (
ImageEditWarning,
RegionPath,
SelectionPath,
crop_pixels_to_alpha,
rasterize_region_path,
rasterize_selection_path,
)
_HIDDEN = {"HIDDEN", "SKIP_SAVE"}
@@ -61,7 +61,7 @@ def _refined_mask_from_pixels(mask_pixels, mask_size, bounds, source_size):
def _edit_image_mask_pixels(
source_pixels,
source_size,
region_mask,
selection_mask,
invert=False,
):
"""Apply a mask while preserving source RGB, then tightly crop the canvas."""
@@ -71,7 +71,7 @@ def _edit_image_mask_pixels(
rgba = np.flipud(
np.asarray(source_pixels, dtype=np.float32).reshape((height, width, 4))
)
mask = np.asarray(region_mask, dtype=np.float32)
mask = np.asarray(selection_mask, dtype=np.float32)
if invert:
mask = 1.0 - mask
mask = np.where(rgba[:, :, 3] > 0.0, mask, 0.0)
@@ -82,13 +82,13 @@ def _edit_image_mask_pixels(
)
def _edit_image_object(source_object, region_mask, invert=False):
def _edit_image_object(source_object, selection_mask, invert=False):
image = source_object.data
source_size = tuple(image.size)
pixels, crop_bounds = _edit_image_mask_pixels(
image_pixels(image),
source_size,
region_mask,
selection_mask,
invert,
)
left, top, right, bottom = crop_bounds
@@ -104,10 +104,10 @@ def _edit_image_object(source_object, region_mask, invert=False):
)
class EditImageMask(ImageRegionGesture, bpy.types.Operator):
refine_region_settings = {
"BOX": "image_box_refine_region",
"LASSO": "image_lasso_refine_region",
class EditImageMask(ImageSelectionGesture, bpy.types.Operator):
refine_selection_settings = {
"BOX": "image_box_refine_selection",
"LASSO": "image_lasso_refine_selection",
}
bl_idname = "anyimage.edit_image_mask"
bl_label = "Edit Image Mask"
@@ -116,8 +116,8 @@ class EditImageMask(ImageRegionGesture, bpy.types.Operator):
source_object_name: bpy.props.StringProperty(options={"HIDDEN", "SKIP_SAVE"})
source_matrix_data: bpy.props.StringProperty(options={"HIDDEN", "SKIP_SAVE"})
region_path_json: bpy.props.StringProperty(options={"HIDDEN", "SKIP_SAVE"})
refine_region: refine_region_property()
selection_path_json: bpy.props.StringProperty(options={"HIDDEN", "SKIP_SAVE"})
refine_selection: refine_selection_property()
invert: bpy.props.BoolProperty(
name="Invert",
description="Keep the opposite side of the refined gesture mask",
@@ -133,23 +133,23 @@ class EditImageMask(ImageRegionGesture, bpy.types.Operator):
self.report({"ERROR"}, str(error))
return {"CANCELLED"}
try:
region_path = RegionPath.from_json(self.region_path_json)
selection_path = SelectionPath.from_json(self.selection_path_json)
if video_total_frames(source_object.data) > 1:
raise RuntimeError("Image tools only support a still image")
region_mask = rasterize_region_path(
selection_mask = rasterize_selection_path(
tuple(source_object.data.size),
region_path,
selection_path,
source_alpha=image_rgba(source_object.data)[:, :, 3],
)
if self.refine_region:
source_path = prepare_region_input(
if self.refine_selection:
source_path = prepare_selection_input(
source_object.data,
region_mask,
selection_mask,
)
result = bpy.ops.anyimage.refine_image_region(
result = bpy.ops.anyimage.refine_image_selection(
input_path=str(source_path),
source_object_name=self.source_object_name,
region_bounds_json=json.dumps(region_mask.bounds),
selection_bounds_json=json.dumps(selection_mask.bounds),
)
if "CANCELLED" in result:
cleanup_image_input(source_path, True)
@@ -157,7 +157,7 @@ class EditImageMask(ImageRegionGesture, bpy.types.Operator):
return {"FINISHED"}
_edit_image_object(
source_object,
region_mask.full_values(tuple(source_object.data.size)),
selection_mask.full_values(tuple(source_object.data.size)),
)
except (json.JSONDecodeError, RuntimeError, TypeError, ValueError) as error:
report_image_edit_exception(self, error)
@@ -165,15 +165,15 @@ class EditImageMask(ImageRegionGesture, bpy.types.Operator):
return {"FINISHED"}
class RefineImageRegion(JobOperatorBase, bpy.types.Operator):
bl_idname = "anyimage.refine_image_region"
bl_label = "Refine Image Region"
job_type = "refine-image-region"
class RefineImageSelection(JobOperatorBase, bpy.types.Operator):
bl_idname = "anyimage.refine_image_selection"
bl_label = "Refine Image Selection"
job_type = "refine-image-selection"
starting_message = "Refining image selection"
input_path: bpy.props.StringProperty(options=_HIDDEN)
source_object_name: bpy.props.StringProperty(options={"HIDDEN", "SKIP_SAVE"})
region_bounds_json: bpy.props.StringProperty(options={"HIDDEN", "SKIP_SAVE"})
selection_bounds_json: bpy.props.StringProperty(options={"HIDDEN", "SKIP_SAVE"})
def request(self, _context):
require_environment()
@@ -187,9 +187,9 @@ class RefineImageRegion(JobOperatorBase, bpy.types.Operator):
def response(self, _context, result):
source_object = require_image_empty(self.source_object_name)
source_bounds = tuple(json.loads(self.region_bounds_json))
source_bounds = tuple(json.loads(self.selection_bounds_json))
local_bounds = tuple(
json.loads(result.file("region_bounds").read_text(encoding="utf-8"))
json.loads(result.file("selection_bounds").read_text(encoding="utf-8"))
)
bounds = (
source_bounds[0] + local_bounds[0],
@@ -198,7 +198,7 @@ class RefineImageRegion(JobOperatorBase, bpy.types.Operator):
source_bounds[1] + local_bounds[3],
)
refined_image = bpy.data.images.load(
str(result.file("region_image")), check_existing=False
str(result.file("selection_image")), check_existing=False
)
try:
refined_pixels = image_pixels(refined_image)
@@ -217,7 +217,7 @@ class RefineImageRegion(JobOperatorBase, bpy.types.Operator):
return str(error)
if "FINISHED" not in bpy.ops.ed.undo_push(message="Edit Image Mask"):
raise RuntimeError("Unable to create the Edit Image Mask Undo step")
return "Image Region refined"
return "Image Selection refined"
def cleanup(self):
cleanup_image_input(self.input_path, True)

View File

@@ -1,6 +1,6 @@
import bpy
from ...common.image_interaction import draw_region_settings
from ...common.viewport import draw_selection_settings
from .edit_mask import EditImageMask
@@ -21,4 +21,4 @@ class ImageLassoTool(bpy.types.WorkSpaceTool):
)
def draw_settings(context, layout, _tool):
draw_region_settings(context, layout, "image_lasso_refine_region")
draw_selection_settings(context, layout, "image_lasso_refine_selection")

View File

@@ -3,14 +3,14 @@ import math
import bpy
from ...common.image_interaction import (
from ...common.viewport import (
deserialize_matrix,
drawing_in_region,
screen_path_to_image_pixels,
serialize_matrix,
)
from ...common.image_region import crop_pixels_to_alpha
from ...common.image_data import (
from ...common.selection import crop_pixels_to_alpha
from ...common.image import (
create_pixel_result_image,
image_base_name,
image_pixels,
@@ -19,7 +19,7 @@ from ...common.image_data import (
require_image_empty,
video_total_frames,
)
from ...common.image_interaction import (
from ...common.viewport import (
active_view3d_tool_id,
draw_dashed_line,
draw_polygon_fill,

View File

@@ -2,7 +2,7 @@ import bpy
from ..properties import BEN2_MODEL_KEY
from ..runtime import JobOperatorBase
from ..common.ai_job import (
from ..common.ai import (
require_background_input_path,
invoke_ai_setup_if_needed,
production_device,
@@ -10,7 +10,7 @@ from ..common.ai_job import (
require_model,
)
from ..common.image_data import (
from ..common.image import (
cleanup_image_input,
default_extract_frames,
is_image_empty,
@@ -70,7 +70,7 @@ class RemoveImageBackground(bpy.types.Operator):
def apply_background_response(result, source_object_name):
from ..common.image_data import (
from ..common.image import (
load_color_result_image,
replace_empty_image,
require_image_empty,

View File

@@ -8,14 +8,14 @@ from ..server.media.resolution import (
MIN_MAX_AI_INPUT_SIZE,
)
from ..runtime import JobOperatorBase
from ..common.ai_job import (
from ..common.ai import (
require_input_path,
invoke_ai_setup_if_needed,
production_device,
require_environment,
require_model,
)
from ..common.image_data import (
from ..common.image import (
cleanup_image_input,
default_extract_frames,
is_image_empty,